Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\RateLimit\Examples;
|
||||
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\RateLimit\SlidingWindowRateLimiter;
|
||||
use App\Framework\RateLimit\SlidingWindowTokenBucket;
|
||||
use App\Framework\SlidingWindow\SlidingWindowFactory;
|
||||
|
||||
/**
|
||||
* Example demonstrating the refactored RateLimit module using SlidingWindow
|
||||
*/
|
||||
final readonly class SlidingWindowRateLimitExample
|
||||
{
|
||||
public function __construct(
|
||||
private Cache $cache
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic rate limiting example
|
||||
*/
|
||||
public function basicRateLimitingExample(): void
|
||||
{
|
||||
$windowFactory = new SlidingWindowFactory($this->cache);
|
||||
$rateLimiter = new SlidingWindowRateLimiter($windowFactory);
|
||||
|
||||
// Check rate limit: 10 requests per 60 seconds
|
||||
$result = $rateLimiter->checkLimit('user:123', 10, 60);
|
||||
|
||||
if ($result->isAllowed()) {
|
||||
echo "Request allowed. {$result->getRemainingRequests()} requests remaining.\n";
|
||||
} else {
|
||||
echo "Rate limit exceeded. Retry after {$result->getRetryAfter()} seconds.\n";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Advanced rate limiting with threat analysis
|
||||
*/
|
||||
public function advancedRateLimitingExample(): void
|
||||
{
|
||||
$windowFactory = new SlidingWindowFactory($this->cache);
|
||||
$rateLimiter = new SlidingWindowRateLimiter($windowFactory);
|
||||
|
||||
$requestContext = [
|
||||
'client_ip' => '192.168.1.100',
|
||||
'user_agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
'request_path' => '/api/data',
|
||||
];
|
||||
|
||||
// Check with advanced analytics
|
||||
$result = $rateLimiter->checkLimitWithAnalysis('api:user:123', 100, 300, $requestContext);
|
||||
|
||||
if ($result->isAttackSuspected()) {
|
||||
echo "SECURITY ALERT: Attack patterns detected!\n";
|
||||
echo "Threat Level: {$result->getThreatLevel()}\n";
|
||||
echo "Attack Patterns: " . implode(', ', $result->attackPatterns) . "\n";
|
||||
|
||||
// Get comprehensive analysis
|
||||
$analysis = $result->toAnalysisArray();
|
||||
echo "Threat Analysis:\n";
|
||||
print_r($analysis['threat_analysis']);
|
||||
}
|
||||
|
||||
if ($result->hasAnomalousTraffic()) {
|
||||
echo "Anomalous traffic detected. Baseline deviation: {$result->baselineDeviation}\n";
|
||||
}
|
||||
|
||||
// Get recommended strategies
|
||||
$strategies = $result->getRecommendedStrategy();
|
||||
if (! empty($strategies)) {
|
||||
echo "Recommended strategies: " . implode(', ', $strategies) . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Token bucket example with SlidingWindow analytics
|
||||
*/
|
||||
public function tokenBucketExample(): void
|
||||
{
|
||||
$windowFactory = new SlidingWindowFactory($this->cache);
|
||||
|
||||
// Create token bucket: 50 capacity, 10 tokens/second refill rate
|
||||
$tokenBucket = new SlidingWindowTokenBucket(
|
||||
identifier: 'api:service:email',
|
||||
capacity: 50,
|
||||
refillRate: 10,
|
||||
windowFactory: $windowFactory
|
||||
);
|
||||
|
||||
// Try to consume 5 tokens
|
||||
$result = $tokenBucket->consume(5);
|
||||
|
||||
if ($result->isAllowed()) {
|
||||
echo "Tokens consumed. {$result->getCurrent()} tokens remaining.\n";
|
||||
} else {
|
||||
echo "Insufficient tokens. Retry after {$result->getRetryAfter()} seconds.\n";
|
||||
}
|
||||
|
||||
// Get analytics
|
||||
$analytics = $tokenBucket->getAnalytics();
|
||||
echo "Token Bucket Analytics:\n";
|
||||
print_r($analytics);
|
||||
}
|
||||
|
||||
/**
|
||||
* Token bucket with threat analysis
|
||||
*/
|
||||
public function tokenBucketWithAnalysisExample(): void
|
||||
{
|
||||
$windowFactory = new SlidingWindowFactory($this->cache);
|
||||
|
||||
$tokenBucket = new SlidingWindowTokenBucket(
|
||||
identifier: 'api:service:sms',
|
||||
capacity: 100,
|
||||
refillRate: 20,
|
||||
windowFactory: $windowFactory
|
||||
);
|
||||
|
||||
$requestContext = [
|
||||
'client_ip' => '10.0.0.50',
|
||||
'user_agent' => 'ApiClient/1.0',
|
||||
'request_path' => '/api/sms/send',
|
||||
];
|
||||
|
||||
// Consume with analysis
|
||||
$result = $tokenBucket->consumeWithAnalysis(10, $requestContext);
|
||||
|
||||
if ($result->isAllowed()) {
|
||||
echo "SMS API tokens consumed successfully.\n";
|
||||
} else {
|
||||
echo "Token bucket exhausted. Please wait {$result->getRetryAfter()} seconds.\n";
|
||||
}
|
||||
|
||||
// Check for suspicious patterns
|
||||
if (isset($result->burstAnalysis['burst_detected']) && $result->burstAnalysis['burst_detected']) {
|
||||
echo "BURST DETECTED: Possible API abuse!\n";
|
||||
echo "Burst intensity: {$result->burstAnalysis['burst_intensity']}\n";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migration example - comparing old vs new implementation
|
||||
*/
|
||||
public function migrationExample(): void
|
||||
{
|
||||
echo "=== Migration Example: Old vs New ===\n";
|
||||
|
||||
// Old approach (still supported for backwards compatibility)
|
||||
$oldRateLimiter = new \App\Framework\RateLimit\RateLimiter(
|
||||
new \App\Framework\RateLimit\Storage\CacheStorage($this->cache)
|
||||
);
|
||||
|
||||
$oldResult = $oldRateLimiter->checkLimit('user:456', 50, 120);
|
||||
echo "Old implementation result: " . ($oldResult->isAllowed() ? 'Allowed' : 'Blocked') . "\n";
|
||||
|
||||
// New approach with SlidingWindow
|
||||
$windowFactory = new SlidingWindowFactory($this->cache);
|
||||
$newRateLimiter = new SlidingWindowRateLimiter($windowFactory);
|
||||
|
||||
$newResult = $newRateLimiter->checkLimitWithAnalysis('user:456', 50, 120, [
|
||||
'client_ip' => '203.0.113.50',
|
||||
'user_agent' => 'Chrome/91.0',
|
||||
]);
|
||||
|
||||
echo "New implementation result: " . ($newResult->isAllowed() ? 'Allowed' : 'Blocked') . "\n";
|
||||
echo "Advanced features available: " . (empty($newResult->attackPatterns) ? 'No threats' : 'Threats detected') . "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Performance comparison example
|
||||
*/
|
||||
public function performanceComparisonExample(): void
|
||||
{
|
||||
echo "=== Performance Comparison ===\n";
|
||||
|
||||
$windowFactory = new SlidingWindowFactory($this->cache);
|
||||
$rateLimiter = new SlidingWindowRateLimiter($windowFactory);
|
||||
|
||||
// Simulate burst of requests
|
||||
$startTime = microtime(true);
|
||||
|
||||
for ($i = 0; $i < 100; $i++) {
|
||||
$result = $rateLimiter->checkLimit("perf:test:{$i}", 10, 60);
|
||||
|
||||
if ($i % 20 === 0) {
|
||||
echo "Request {$i}: " . ($result->isAllowed() ? 'OK' : 'Limited') . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
$endTime = microtime(true);
|
||||
$processingTime = ($endTime - $startTime) * 1000; // Convert to milliseconds
|
||||
|
||||
echo "Processed 100 rate limit checks in " . number_format($processingTime, 2) . "ms\n";
|
||||
echo "Average: " . ($processingTime / 100) . "ms per check\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Run all examples
|
||||
*/
|
||||
public function runAllExamples(): void
|
||||
{
|
||||
echo "=== SlidingWindow RateLimit Examples ===\n\n";
|
||||
|
||||
echo "1. Basic Rate Limiting:\n";
|
||||
$this->basicRateLimitingExample();
|
||||
echo "\n";
|
||||
|
||||
echo "2. Advanced Rate Limiting with Threat Analysis:\n";
|
||||
$this->advancedRateLimitingExample();
|
||||
echo "\n";
|
||||
|
||||
echo "3. Token Bucket:\n";
|
||||
$this->tokenBucketExample();
|
||||
echo "\n";
|
||||
|
||||
echo "4. Token Bucket with Analysis:\n";
|
||||
$this->tokenBucketWithAnalysisExample();
|
||||
echo "\n";
|
||||
|
||||
echo "5. Migration Example:\n";
|
||||
$this->migrationExample();
|
||||
echo "\n";
|
||||
|
||||
echo "6. Performance Comparison:\n";
|
||||
$this->performanceComparisonExample();
|
||||
echo "\n";
|
||||
|
||||
echo "=== Examples Complete ===\n";
|
||||
}
|
||||
}
|
||||
338
src/Framework/RateLimit/MIGRATION.md
Normal file
338
src/Framework/RateLimit/MIGRATION.md
Normal file
@@ -0,0 +1,338 @@
|
||||
# RateLimit to SlidingWindow Migration Guide
|
||||
|
||||
This guide explains how to migrate from the legacy RateLimit implementation to the new SlidingWindow-based system.
|
||||
|
||||
## Overview
|
||||
|
||||
The RateLimit module has been refactored to use the SlidingWindow module for better performance, accuracy, and advanced analytics. The new implementation provides:
|
||||
|
||||
- **Improved Accuracy**: True sliding window instead of fixed windows
|
||||
- **Advanced Analytics**: Burst detection, threat analysis, and pattern recognition
|
||||
- **Better Performance**: Optimized data structures and caching
|
||||
- **Unified Architecture**: Consistent with other framework modules
|
||||
|
||||
## Key Changes
|
||||
|
||||
### 1. New Classes
|
||||
|
||||
- `SlidingWindowRateLimiter`: Modern rate limiter using SlidingWindow
|
||||
- `SlidingWindowTokenBucket`: Token bucket with analytics
|
||||
- `RateLimitAggregator`: Advanced analytics aggregator
|
||||
- `RateLimitResult`: Enhanced result object (already existing)
|
||||
|
||||
### 2. Backwards Compatibility
|
||||
|
||||
The old `RateLimiter` class is still available and functional. No breaking changes to existing API.
|
||||
|
||||
## Migration Steps
|
||||
|
||||
### Step 1: Update Dependency Injection
|
||||
|
||||
```php
|
||||
// Old way (still works)
|
||||
$rateLimiter = $container->get(RateLimiter::class);
|
||||
|
||||
// New way (recommended)
|
||||
$rateLimiter = $container->get(SlidingWindowRateLimiter::class);
|
||||
|
||||
// Or use alias
|
||||
$rateLimiter = $container->get('rate_limiter'); // Points to SlidingWindow version
|
||||
```
|
||||
|
||||
### Step 2: Basic Rate Limiting Migration
|
||||
|
||||
```php
|
||||
// Old implementation
|
||||
$rateLimiter = new RateLimiter(new CacheStorage($cache));
|
||||
$result = $rateLimiter->checkLimit('user:123', 10, 60);
|
||||
|
||||
// New implementation
|
||||
$windowFactory = new SlidingWindowFactory($cache);
|
||||
$rateLimiter = new SlidingWindowRateLimiter($windowFactory);
|
||||
$result = $rateLimiter->checkLimit('user:123', 10, 60);
|
||||
|
||||
// API is identical, but implementation is more accurate
|
||||
```
|
||||
|
||||
### Step 3: Enhanced Analytics (New Feature)
|
||||
|
||||
```php
|
||||
// New advanced analytics capability
|
||||
$requestContext = [
|
||||
'client_ip' => $_SERVER['REMOTE_ADDR'],
|
||||
'user_agent' => $_SERVER['HTTP_USER_AGENT'],
|
||||
'request_path' => $_SERVER['REQUEST_URI']
|
||||
];
|
||||
|
||||
$result = $rateLimiter->checkLimitWithAnalysis('user:123', 10, 60, $requestContext);
|
||||
|
||||
// Check for security threats
|
||||
if ($result->isAttackSuspected()) {
|
||||
// Handle potential attack
|
||||
$threatLevel = $result->getThreatLevel(); // 'low', 'medium', 'high', 'critical'
|
||||
$patterns = $result->attackPatterns; // ['burst_attack', 'volumetric_attack']
|
||||
}
|
||||
|
||||
// Check for anomalous traffic
|
||||
if ($result->hasAnomalousTraffic()) {
|
||||
$deviation = $result->baselineDeviation;
|
||||
// Implement adaptive response
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Token Bucket Migration
|
||||
|
||||
```php
|
||||
// Old token bucket (through RateLimiter)
|
||||
$result = $rateLimiter->checkTokenBucket('api:service', 100, 10, 5);
|
||||
|
||||
// New dedicated token bucket with analytics
|
||||
$tokenBucket = new SlidingWindowTokenBucket(
|
||||
identifier: 'api:service',
|
||||
capacity: 100,
|
||||
refillRate: 10,
|
||||
windowFactory: $windowFactory
|
||||
);
|
||||
|
||||
$result = $tokenBucket->consumeWithAnalysis(5, $requestContext);
|
||||
```
|
||||
|
||||
### Step 5: Update Middleware/Controllers
|
||||
|
||||
```php
|
||||
class RateLimitMiddleware
|
||||
{
|
||||
public function __construct(
|
||||
// Old
|
||||
// private RateLimiter $rateLimiter
|
||||
|
||||
// New
|
||||
private SlidingWindowRateLimiter $rateLimiter
|
||||
) {}
|
||||
|
||||
public function handle($request, $next)
|
||||
{
|
||||
$clientId = $this->getClientId($request);
|
||||
|
||||
// Enhanced with threat analysis
|
||||
$result = $this->rateLimiter->checkLimitWithAnalysis(
|
||||
$clientId,
|
||||
100,
|
||||
300,
|
||||
[
|
||||
'client_ip' => $request->getClientIp(),
|
||||
'user_agent' => $request->getUserAgent(),
|
||||
'request_path' => $request->getPath()
|
||||
]
|
||||
);
|
||||
|
||||
if (!$result->isAllowed()) {
|
||||
return $this->createRateLimitResponse($result);
|
||||
}
|
||||
|
||||
// Log security events
|
||||
if ($result->shouldLogSecurityEvent()) {
|
||||
$this->logSecurityEvent($result);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Changes
|
||||
|
||||
### Environment Variables
|
||||
|
||||
No changes to existing environment variables. New optional variables:
|
||||
|
||||
```env
|
||||
# SlidingWindow-specific settings
|
||||
RATE_LIMIT_PERSISTENT=true
|
||||
RATE_LIMIT_BURST_THRESHOLD=0.8
|
||||
RATE_LIMIT_MIN_INTERVAL_THRESHOLD=2.0
|
||||
|
||||
# Advanced analytics
|
||||
RATE_LIMIT_THREAT_ANALYSIS_ENABLED=true
|
||||
RATE_LIMIT_BASELINE_TRACKING_ENABLED=true
|
||||
```
|
||||
|
||||
### Container Configuration
|
||||
|
||||
```php
|
||||
// In your service provider or bootstrap
|
||||
$container->bind(SlidingWindowRateLimiter::class, function ($container) {
|
||||
$cache = $container->get(Cache::class);
|
||||
$windowFactory = new SlidingWindowFactory($cache);
|
||||
|
||||
return new SlidingWindowRateLimiter(
|
||||
$windowFactory,
|
||||
persistent: $_ENV['RATE_LIMIT_PERSISTENT'] ?? true
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
## Performance Improvements
|
||||
|
||||
### Memory Usage
|
||||
- **Reduced**: Optimized data structures in SlidingWindow module
|
||||
- **Efficient**: Smart cleanup of expired data
|
||||
|
||||
### Processing Speed
|
||||
- **Faster**: Batch operations and optimized algorithms
|
||||
- **Scalable**: Better performance under high load
|
||||
|
||||
### Accuracy
|
||||
- **Precise**: True sliding window vs. fixed window approximation
|
||||
- **Consistent**: Uniform distribution of requests over time
|
||||
|
||||
## New Features
|
||||
|
||||
### 1. Burst Detection
|
||||
```php
|
||||
$analytics = $rateLimiter->getAdvancedAnalytics('user:123', 60);
|
||||
if ($analytics['burst_analysis']['burst_detected']) {
|
||||
// Handle burst attack
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Threat Analysis
|
||||
```php
|
||||
$result = $rateLimiter->checkLimitWithAnalysis($key, $limit, $window, $context);
|
||||
$threatScore = $result->threatScore->getValue(); // 0-100
|
||||
$recommendedActions = $result->getRecommendedStrategy();
|
||||
```
|
||||
|
||||
### 3. Adaptive Responses
|
||||
```php
|
||||
// Automatically adjusts retry-after based on patterns
|
||||
$retryAfter = $result->adaptiveRetryAfter->toSeconds();
|
||||
|
||||
// Multiple response strategies
|
||||
foreach ($result->responseStrategies as $strategy) {
|
||||
match ($strategy) {
|
||||
'immediate_block' => $this->blockImmediately(),
|
||||
'enhanced_monitoring' => $this->enableMonitoring(),
|
||||
'captcha_challenge' => $this->requireCaptcha(),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Migration
|
||||
|
||||
### Unit Tests
|
||||
```php
|
||||
class SlidingWindowRateLimiterTest extends TestCase
|
||||
{
|
||||
public function test_basic_rate_limiting()
|
||||
{
|
||||
$cache = new InMemoryCache();
|
||||
$windowFactory = new SlidingWindowFactory($cache);
|
||||
$rateLimiter = new SlidingWindowRateLimiter($windowFactory);
|
||||
|
||||
// Test basic functionality
|
||||
$result = $rateLimiter->checkLimit('test', 5, 60);
|
||||
$this->assertTrue($result->isAllowed());
|
||||
}
|
||||
|
||||
public function test_threat_detection()
|
||||
{
|
||||
// Test burst pattern detection
|
||||
// Test anomaly detection
|
||||
// Test attack pattern recognition
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Integration Tests
|
||||
```php
|
||||
class RateLimitIntegrationTest extends TestCase
|
||||
{
|
||||
public function test_middleware_integration()
|
||||
{
|
||||
// Test with actual HTTP requests
|
||||
// Test threat detection in middleware
|
||||
// Test performance under load
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Monitoring and Observability
|
||||
|
||||
### Metrics to Track
|
||||
```php
|
||||
// Basic metrics (existing)
|
||||
- rate_limit_requests_allowed
|
||||
- rate_limit_requests_blocked
|
||||
- rate_limit_processing_time
|
||||
|
||||
// New metrics
|
||||
- rate_limit_threats_detected
|
||||
- rate_limit_burst_patterns
|
||||
- rate_limit_baseline_deviations
|
||||
- rate_limit_adaptive_responses
|
||||
```
|
||||
|
||||
### Logging
|
||||
```php
|
||||
// Enhanced logging with threat context
|
||||
if ($result->shouldLogSecurityEvent()) {
|
||||
$logger->warning('Rate limit security event', [
|
||||
'client_ip' => $result->clientIp,
|
||||
'threat_level' => $result->getThreatLevel(),
|
||||
'attack_patterns' => $result->attackPatterns,
|
||||
'recommended_actions' => $result->getRecommendedStrategy()
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
If issues arise, you can easily rollback:
|
||||
|
||||
1. **Switch DI binding**:
|
||||
```php
|
||||
// Change from
|
||||
$container->bind('rate_limiter', SlidingWindowRateLimiter::class);
|
||||
// Back to
|
||||
$container->bind('rate_limiter', RateLimiter::class);
|
||||
```
|
||||
|
||||
2. **Update middleware**: Change constructor injection back to `RateLimiter`
|
||||
|
||||
3. **Remove new features**: Comment out threat analysis code
|
||||
|
||||
The old implementation remains fully functional and tested.
|
||||
|
||||
## Timeline
|
||||
|
||||
### Phase 1: Preparation (Week 1)
|
||||
- Update DI container configuration
|
||||
- Deploy alongside old implementation
|
||||
- Update monitoring
|
||||
|
||||
### Phase 2: Gradual Migration (Week 2-3)
|
||||
- Migrate non-critical endpoints
|
||||
- Monitor performance and accuracy
|
||||
- Test threat detection
|
||||
|
||||
### Phase 3: Full Migration (Week 4)
|
||||
- Migrate critical endpoints
|
||||
- Enable advanced analytics
|
||||
- Full monitoring deployment
|
||||
|
||||
### Phase 4: Optimization (Week 5)
|
||||
- Fine-tune threat detection thresholds
|
||||
- Optimize performance based on metrics
|
||||
- Remove old implementation if stable
|
||||
|
||||
## Support
|
||||
|
||||
For questions or issues during migration:
|
||||
|
||||
1. Check existing unit tests for usage examples
|
||||
2. Review the `Examples/SlidingWindowRateLimitExample.php` file
|
||||
3. Consult the SlidingWindow module documentation
|
||||
4. Test thoroughly in staging environment
|
||||
|
||||
The migration is designed to be gradual and safe, with full backwards compatibility maintained.
|
||||
201
src/Framework/RateLimit/RateLimitConfig.php
Normal file
201
src/Framework/RateLimit/RateLimitConfig.php
Normal file
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\RateLimit;
|
||||
|
||||
use App\Framework\Config\SecurityConfig;
|
||||
|
||||
/**
|
||||
* Rate Limiting Configuration
|
||||
*
|
||||
* Migrated from Firewall module to RateLimit module.
|
||||
* Now integrated with SlidingWindow for improved accuracy.
|
||||
*/
|
||||
final readonly class RateLimitConfig
|
||||
{
|
||||
public function __construct(
|
||||
public bool $enabled = true,
|
||||
public int $requestsPerMinute = 60,
|
||||
public int $requestsPerHour = 1000,
|
||||
public int $requestsPerDay = 10000,
|
||||
public int $burstLimit = 10,
|
||||
public float $windowSize = 60.0, // seconds
|
||||
public bool $enableBurstDetection = true,
|
||||
public float $threatThreshold = 0.8,
|
||||
public array $exemptPaths = ['/health', '/ping', '/metrics'],
|
||||
public array $trustedIps = ['127.0.0.1', '::1'],
|
||||
public bool $logViolations = true,
|
||||
public bool $blockOnViolation = true,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create configuration from SecurityConfig
|
||||
*/
|
||||
public static function fromSecurityConfig(SecurityConfig $securityConfig): self
|
||||
{
|
||||
// Todo: This should use the AppConfig instead of the SecurityConfig
|
||||
return match($securityConfig->appKey) {
|
||||
'production' => self::production(),
|
||||
'development' => self::development(),
|
||||
'testing' => self::testing(),
|
||||
default => self::development()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Production configuration (strict limits)
|
||||
*/
|
||||
public static function production(): self
|
||||
{
|
||||
return new self(
|
||||
enabled: true,
|
||||
requestsPerMinute: 30,
|
||||
requestsPerHour: 500,
|
||||
requestsPerDay: 5000,
|
||||
burstLimit: 5,
|
||||
windowSize: 60.0,
|
||||
enableBurstDetection: true,
|
||||
threatThreshold: 0.7,
|
||||
exemptPaths: ['/health'],
|
||||
trustedIps: [],
|
||||
logViolations: true,
|
||||
blockOnViolation: true,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Development configuration (permissive)
|
||||
*/
|
||||
public static function development(): self
|
||||
{
|
||||
return new self(
|
||||
enabled: true,
|
||||
requestsPerMinute: 120,
|
||||
requestsPerHour: 2000,
|
||||
requestsPerDay: 20000,
|
||||
burstLimit: 20,
|
||||
windowSize: 60.0,
|
||||
enableBurstDetection: false,
|
||||
threatThreshold: 0.9,
|
||||
exemptPaths: ['/health', '/ping', '/metrics', '/debug', '/api/test'],
|
||||
trustedIps: ['127.0.0.1', '::1', '192.168.0.0/16', '10.0.0.0/8'],
|
||||
logViolations: true,
|
||||
blockOnViolation: false, // Log only in development
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing configuration (disabled)
|
||||
*/
|
||||
public static function testing(): self
|
||||
{
|
||||
return new self(
|
||||
enabled: false,
|
||||
requestsPerMinute: 1000,
|
||||
requestsPerHour: 10000,
|
||||
requestsPerDay: 100000,
|
||||
burstLimit: 100,
|
||||
windowSize: 1.0,
|
||||
enableBurstDetection: false,
|
||||
threatThreshold: 1.0,
|
||||
exemptPaths: ['*'],
|
||||
trustedIps: ['127.0.0.1', '::1'],
|
||||
logViolations: false,
|
||||
blockOnViolation: false,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if path is exempt from rate limiting
|
||||
*/
|
||||
public function isExemptPath(string $path): bool
|
||||
{
|
||||
foreach ($this->exemptPaths as $exemptPath) {
|
||||
if ($exemptPath === '*' || str_starts_with($path, $exemptPath)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if IP is trusted
|
||||
*/
|
||||
public function isTrustedIp(string $ip): bool
|
||||
{
|
||||
foreach ($this->trustedIps as $trustedIp) {
|
||||
if ($this->ipMatches($ip, $trustedIp)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if IP matches pattern (supports CIDR)
|
||||
*/
|
||||
private function ipMatches(string $ip, string $pattern): bool
|
||||
{
|
||||
// Exact match
|
||||
if ($ip === $pattern) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// CIDR match
|
||||
if (str_contains($pattern, '/')) {
|
||||
[$network, $maskBits] = explode('/', $pattern, 2);
|
||||
$maskBits = (int) $maskBits;
|
||||
|
||||
// IPv4 CIDR
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) &&
|
||||
filter_var($network, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
$ipLong = ip2long($ip);
|
||||
$networkLong = ip2long($network);
|
||||
$mask = -1 << (32 - $maskBits);
|
||||
|
||||
return ($ipLong & $mask) === ($networkLong & $mask);
|
||||
}
|
||||
|
||||
// IPv6 CIDR (simplified)
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) &&
|
||||
filter_var($network, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||
// For IPv6, we'd need more complex subnet matching
|
||||
// For now, just do exact match for IPv6
|
||||
return $ip === $network;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to array for serialization
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'enabled' => $this->enabled,
|
||||
'limits' => [
|
||||
'requests_per_minute' => $this->requestsPerMinute,
|
||||
'requests_per_hour' => $this->requestsPerHour,
|
||||
'requests_per_day' => $this->requestsPerDay,
|
||||
'burst_limit' => $this->burstLimit,
|
||||
],
|
||||
'window' => [
|
||||
'size_seconds' => $this->windowSize,
|
||||
'burst_detection' => $this->enableBurstDetection,
|
||||
'threat_threshold' => $this->threatThreshold,
|
||||
],
|
||||
'security' => [
|
||||
'exempt_paths_count' => count($this->exemptPaths),
|
||||
'trusted_ips_count' => count($this->trustedIps),
|
||||
'log_violations' => $this->logViolations,
|
||||
'block_on_violation' => $this->blockOnViolation,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -4,14 +4,50 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\RateLimit;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\Core\ValueObjects\Percentage;
|
||||
use App\Framework\Core\ValueObjects\Timestamp;
|
||||
|
||||
/**
|
||||
* Rate Limit Result with Advanced WAF Intelligence
|
||||
*
|
||||
* Enhanced with sophisticated threat analysis, attack pattern detection,
|
||||
* and adaptive response capabilities from the WAF system.
|
||||
*/
|
||||
readonly class RateLimitResult
|
||||
{
|
||||
public function __construct(
|
||||
private bool $allowed,
|
||||
private int $limit,
|
||||
private int $current,
|
||||
private ?int $retryAfter = null
|
||||
) {}
|
||||
private ?int $retryAfter = null,
|
||||
|
||||
// WAF Intelligence Extensions
|
||||
public ?Percentage $threatScore = null,
|
||||
public ?float $baselineDeviation = null,
|
||||
public array $attackPatterns = [],
|
||||
public array $anomalyIndicators = [],
|
||||
public ?Duration $processingTime = null,
|
||||
public ?Timestamp $analysisTimestamp = null,
|
||||
public ?string $clientIp = null,
|
||||
public ?string $requestPath = null,
|
||||
public ?string $userAgent = null,
|
||||
|
||||
// Traffic Analysis Extensions
|
||||
public ?array $trafficAnalysis = null,
|
||||
public ?array $burstAnalysis = null,
|
||||
public ?array $geoAnalysis = null,
|
||||
public ?float $confidence = null,
|
||||
|
||||
// Adaptive Response Data
|
||||
public ?string $recommendedAction = null,
|
||||
public ?Duration $adaptiveRetryAfter = null,
|
||||
public array $responseStrategies = [],
|
||||
|
||||
// Performance Metrics
|
||||
public ?array $performanceMetrics = null
|
||||
) {
|
||||
}
|
||||
|
||||
public static function allowed(int $limit, int $current): self
|
||||
{
|
||||
@@ -32,6 +68,72 @@ readonly class RateLimitResult
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create result with advanced threat analysis
|
||||
*/
|
||||
public static function withThreatAnalysis(
|
||||
bool $allowed,
|
||||
int $limit,
|
||||
int $current,
|
||||
array $analysisData = [],
|
||||
?int $retryAfter = null
|
||||
): self {
|
||||
return new self(
|
||||
allowed: $allowed,
|
||||
limit: $limit,
|
||||
current: $current,
|
||||
retryAfter: $retryAfter,
|
||||
threatScore: isset($analysisData['threat_score']) ? Percentage::from($analysisData['threat_score']) : null,
|
||||
baselineDeviation: $analysisData['baseline_deviation'] ?? null,
|
||||
attackPatterns: $analysisData['attack_patterns'] ?? [],
|
||||
anomalyIndicators: $analysisData['anomaly_indicators'] ?? [],
|
||||
processingTime: $analysisData['processing_time'] ?? null,
|
||||
analysisTimestamp: $analysisData['timestamp'] ?? null,
|
||||
clientIp: $analysisData['client_ip'] ?? null,
|
||||
requestPath: $analysisData['request_path'] ?? null,
|
||||
userAgent: $analysisData['user_agent'] ?? null,
|
||||
trafficAnalysis: $analysisData['traffic_analysis'] ?? null,
|
||||
burstAnalysis: $analysisData['burst_analysis'] ?? null,
|
||||
geoAnalysis: $analysisData['geo_analysis'] ?? null,
|
||||
confidence: $analysisData['confidence'] ?? null,
|
||||
recommendedAction: $analysisData['recommended_action'] ?? ($allowed ? 'allow' : 'block'),
|
||||
adaptiveRetryAfter: $analysisData['adaptive_retry_after'] ?? null,
|
||||
responseStrategies: $analysisData['response_strategies'] ?? [],
|
||||
performanceMetrics: $analysisData['performance_metrics'] ?? null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create suspicious result (allowed but flagged)
|
||||
*/
|
||||
public static function suspicious(
|
||||
int $limit,
|
||||
int $current,
|
||||
array $suspicionData = []
|
||||
): self {
|
||||
return new self(
|
||||
allowed: true,
|
||||
limit: $limit,
|
||||
current: $current,
|
||||
threatScore: isset($suspicionData['threat_score']) ? Percentage::from($suspicionData['threat_score']) : Percentage::from(60.0),
|
||||
baselineDeviation: $suspicionData['baseline_deviation'] ?? null,
|
||||
attackPatterns: $suspicionData['attack_patterns'] ?? [],
|
||||
anomalyIndicators: $suspicionData['anomaly_indicators'] ?? [],
|
||||
processingTime: $suspicionData['processing_time'] ?? null,
|
||||
analysisTimestamp: $suspicionData['timestamp'] ?? null,
|
||||
clientIp: $suspicionData['client_ip'] ?? null,
|
||||
requestPath: $suspicionData['request_path'] ?? null,
|
||||
userAgent: $suspicionData['user_agent'] ?? null,
|
||||
trafficAnalysis: $suspicionData['traffic_analysis'] ?? null,
|
||||
burstAnalysis: $suspicionData['burst_analysis'] ?? null,
|
||||
geoAnalysis: $suspicionData['geo_analysis'] ?? null,
|
||||
confidence: $suspicionData['confidence'] ?? null,
|
||||
recommendedAction: 'monitor',
|
||||
responseStrategies: $suspicionData['response_strategies'] ?? ['increased_monitoring', 'captcha_challenge'],
|
||||
performanceMetrics: $suspicionData['performance_metrics'] ?? null
|
||||
);
|
||||
}
|
||||
|
||||
public function isAllowed(): bool
|
||||
{
|
||||
return $this->allowed;
|
||||
@@ -39,7 +141,7 @@ readonly class RateLimitResult
|
||||
|
||||
public function isExceeded(): bool
|
||||
{
|
||||
return !$this->allowed;
|
||||
return ! $this->allowed;
|
||||
}
|
||||
|
||||
public function getLimit(): int
|
||||
@@ -61,4 +163,120 @@ readonly class RateLimitResult
|
||||
{
|
||||
return max(0, $this->limit - $this->current);
|
||||
}
|
||||
|
||||
// ===== WAF Intelligence Methods =====
|
||||
|
||||
/**
|
||||
* Check if request shows signs of attack
|
||||
*/
|
||||
public function isAttackSuspected(): bool
|
||||
{
|
||||
return $this->threatScore?->getValue() >= 70.0 || ! empty($this->attackPatterns);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if request shows baseline deviation
|
||||
*/
|
||||
public function hasAnomalousTraffic(): bool
|
||||
{
|
||||
return $this->baselineDeviation !== null && abs($this->baselineDeviation) >= 3.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get threat level classification
|
||||
*/
|
||||
public function getThreatLevel(): string
|
||||
{
|
||||
if ($this->threatScore === null) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
$score = $this->threatScore->getValue();
|
||||
|
||||
return match (true) {
|
||||
$score >= 90.0 => 'critical',
|
||||
$score >= 70.0 => 'high',
|
||||
$score >= 40.0 => 'medium',
|
||||
$score >= 20.0 => 'low',
|
||||
default => 'minimal'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recommended response strategy
|
||||
*/
|
||||
public function getRecommendedStrategy(): array
|
||||
{
|
||||
$strategies = [];
|
||||
|
||||
if ($this->isAttackSuspected()) {
|
||||
$strategies[] = 'immediate_block';
|
||||
$strategies[] = 'security_alert';
|
||||
}
|
||||
|
||||
if ($this->hasAnomalousTraffic()) {
|
||||
$strategies[] = 'enhanced_monitoring';
|
||||
$strategies[] = 'adaptive_rate_limiting';
|
||||
}
|
||||
|
||||
if (! empty($this->anomalyIndicators)) {
|
||||
$strategies[] = 'behavior_analysis';
|
||||
}
|
||||
|
||||
return array_merge($strategies, $this->responseStrategies);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if result warrants security event logging
|
||||
*/
|
||||
public function shouldLogSecurityEvent(): bool
|
||||
{
|
||||
return $this->isAttackSuspected() ||
|
||||
$this->hasAnomalousTraffic() ||
|
||||
! $this->isAllowed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to comprehensive analysis array
|
||||
*/
|
||||
public function toAnalysisArray(): array
|
||||
{
|
||||
return [
|
||||
'basic_result' => [
|
||||
'allowed' => $this->isAllowed(),
|
||||
'limit' => $this->getLimit(),
|
||||
'current' => $this->getCurrent(),
|
||||
'retry_after' => $this->getRetryAfter(),
|
||||
'remaining' => $this->getRemainingRequests(),
|
||||
],
|
||||
'threat_analysis' => [
|
||||
'threat_score' => $this->threatScore?->getValue(),
|
||||
'threat_level' => $this->getThreatLevel(),
|
||||
'baseline_deviation' => $this->baselineDeviation,
|
||||
'attack_patterns' => $this->attackPatterns,
|
||||
'anomaly_indicators' => $this->anomalyIndicators,
|
||||
'confidence' => $this->confidence,
|
||||
],
|
||||
'request_context' => [
|
||||
'client_ip' => $this->clientIp,
|
||||
'request_path' => $this->requestPath,
|
||||
'user_agent' => $this->userAgent,
|
||||
'analysis_timestamp' => $this->analysisTimestamp?->toIsoString(),
|
||||
],
|
||||
'traffic_patterns' => [
|
||||
'traffic_analysis' => $this->trafficAnalysis,
|
||||
'burst_analysis' => $this->burstAnalysis,
|
||||
'geo_analysis' => $this->geoAnalysis,
|
||||
],
|
||||
'response_guidance' => [
|
||||
'recommended_action' => $this->recommendedAction,
|
||||
'adaptive_retry_after' => $this->adaptiveRetryAfter?->toSeconds(),
|
||||
'response_strategies' => $this->getRecommendedStrategy(),
|
||||
],
|
||||
'performance' => [
|
||||
'processing_time' => $this->processingTime?->toMilliseconds(),
|
||||
'performance_metrics' => $this->performanceMetrics,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,33 +5,34 @@ declare(strict_types=1);
|
||||
namespace App\Framework\RateLimit;
|
||||
|
||||
use App\Framework\RateLimit\Storage\StorageInterface;
|
||||
use App\Framework\RateLimit\TimeProvider\TimeProviderInterface;
|
||||
use App\Framework\RateLimit\TimeProvider\SystemTimeProvider;
|
||||
use App\Framework\RateLimit\TimeProvider\TimeProviderInterface;
|
||||
|
||||
final readonly class RateLimiter
|
||||
{
|
||||
public function __construct(
|
||||
private StorageInterface $storage,
|
||||
private TimeProviderInterface $timeProvider = new SystemTimeProvider()
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
public function checkLimit(string $key, int $limit, int $window): RateLimitResult
|
||||
public function checkLimit(string $key, int $limit, float $window): RateLimitResult
|
||||
{
|
||||
$now = $this->timeProvider->getCurrentTime();
|
||||
$windowStart = $now - $window;
|
||||
$windowStart = $now - (int) $window;
|
||||
|
||||
// Sliding Window Log Implementierung
|
||||
$requests = $this->storage->getRequestsInWindow($key, $windowStart, $now);
|
||||
|
||||
if (count($requests) >= $limit) {
|
||||
$oldestRequest = min($requests);
|
||||
$retryAfter = $oldestRequest + $window - $now;
|
||||
$retryAfter = $oldestRequest + (int) $window - $now;
|
||||
|
||||
return RateLimitResult::exceeded($limit, count($requests), max(1, $retryAfter));
|
||||
}
|
||||
|
||||
// Request hinzufügen
|
||||
$this->storage->addRequest($key, $now, $window);
|
||||
$this->storage->addRequest($key, $now, (int) $window);
|
||||
|
||||
return RateLimitResult::allowed($limit, count($requests) + 1);
|
||||
}
|
||||
@@ -70,6 +71,7 @@ final readonly class RateLimiter
|
||||
$this->storage->saveTokenBucket($key, $bucket);
|
||||
|
||||
$retryAfter = intval(($tokens - $bucket->tokens) / $refillRate);
|
||||
|
||||
return RateLimitResult::exceeded($capacity, $bucket->tokens, $retryAfter);
|
||||
}
|
||||
|
||||
@@ -78,11 +80,218 @@ final readonly class RateLimiter
|
||||
$this->storage->clear($key);
|
||||
}
|
||||
|
||||
public function getUsage(string $key, int $window): int
|
||||
public function getUsage(string $key, float $window): int
|
||||
{
|
||||
$now = $this->timeProvider->getCurrentTime();
|
||||
$windowStart = $now - $window;
|
||||
$windowStart = $now - (int) $window;
|
||||
|
||||
return count($this->storage->getRequestsInWindow($key, $windowStart, $now));
|
||||
}
|
||||
|
||||
// ===== WAF Intelligence Extensions =====
|
||||
|
||||
/**
|
||||
* Advanced rate limiting with threat analysis
|
||||
*/
|
||||
public function checkLimitWithAnalysis(
|
||||
string $key,
|
||||
int $limit,
|
||||
float $window,
|
||||
array $requestContext = []
|
||||
): RateLimitResult {
|
||||
$basicResult = $this->checkLimit($key, $limit, $window);
|
||||
|
||||
// If no context provided, return basic result
|
||||
if (empty($requestContext)) {
|
||||
return $basicResult;
|
||||
}
|
||||
|
||||
// Perform traffic pattern analysis
|
||||
$analysisData = $this->analyzeTrafficPatterns($key, $window, $requestContext);
|
||||
|
||||
return RateLimitResult::withThreatAnalysis(
|
||||
$basicResult->isAllowed(),
|
||||
$basicResult->getLimit(),
|
||||
$basicResult->getCurrent(),
|
||||
$analysisData,
|
||||
$basicResult->getRetryAfter()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for burst patterns (potential attack indicator)
|
||||
*/
|
||||
public function detectBurstPattern(string $key, float $window = 60.0): array
|
||||
{
|
||||
$now = $this->timeProvider->getCurrentTime();
|
||||
$requests = $this->storage->getRequestsInWindow($key, $now - (int) $window, $now);
|
||||
|
||||
if (count($requests) < 5) {
|
||||
return ['burst_detected' => false, 'burst_intensity' => 0.0];
|
||||
}
|
||||
|
||||
sort($requests);
|
||||
$intervals = [];
|
||||
|
||||
for ($i = 1; $i < count($requests); $i++) {
|
||||
$intervals[] = $requests[$i] - $requests[$i - 1];
|
||||
}
|
||||
|
||||
// Calculate burst intensity (lower intervals = higher intensity)
|
||||
$avgInterval = array_sum($intervals) / count($intervals);
|
||||
$minInterval = min($intervals);
|
||||
|
||||
$burstIntensity = $avgInterval > 0 ? ($avgInterval - $minInterval) / $avgInterval : 1.0;
|
||||
$burstDetected = $burstIntensity > 0.8 && $minInterval < 2; // Very fast requests
|
||||
|
||||
return [
|
||||
'burst_detected' => $burstDetected,
|
||||
'burst_intensity' => $burstIntensity,
|
||||
'min_interval' => $minInterval,
|
||||
'avg_interval' => $avgInterval,
|
||||
'request_count' => count($requests),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate baseline deviation for anomaly detection
|
||||
*/
|
||||
public function calculateBaselineDeviation(string $baseKey, int $currentRate, int $analysisWindow = 3600): float
|
||||
{
|
||||
$historicalKey = $baseKey . ':baseline';
|
||||
$baseline = $this->storage->getBaseline($historicalKey) ?? [];
|
||||
|
||||
if (empty($baseline)) {
|
||||
// Initialize baseline
|
||||
$this->storage->updateBaseline($historicalKey, $currentRate);
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$mean = array_sum($baseline) / count($baseline);
|
||||
$variance = array_sum(array_map(fn ($x) => pow($x - $mean, 2), $baseline)) / count($baseline);
|
||||
$stdDev = sqrt($variance);
|
||||
|
||||
// Update baseline with current rate
|
||||
$this->storage->updateBaseline($historicalKey, $currentRate);
|
||||
|
||||
return $stdDev > 0 ? ($currentRate - $mean) / $stdDev : 0.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze traffic patterns for threat detection
|
||||
*/
|
||||
private function analyzeTrafficPatterns(string $key, float $window, array $requestContext): array
|
||||
{
|
||||
$now = $this->timeProvider->getCurrentTime();
|
||||
$currentRate = $this->getUsage($key, $window);
|
||||
|
||||
// Burst analysis
|
||||
$burstAnalysis = $this->detectBurstPattern($key, $window);
|
||||
|
||||
// Baseline deviation
|
||||
$baselineDeviation = $this->calculateBaselineDeviation($key, $currentRate);
|
||||
|
||||
// Calculate threat score
|
||||
$threatScore = $this->calculateThreatScore($burstAnalysis, $baselineDeviation, $requestContext);
|
||||
|
||||
// Identify attack patterns
|
||||
$attackPatterns = $this->identifyAttackPatterns($requestContext, $burstAnalysis);
|
||||
|
||||
// Anomaly indicators
|
||||
$anomalyIndicators = $this->identifyAnomalyIndicators($burstAnalysis, $baselineDeviation);
|
||||
|
||||
return [
|
||||
'threat_score' => $threatScore,
|
||||
'baseline_deviation' => $baselineDeviation,
|
||||
'attack_patterns' => $attackPatterns,
|
||||
'anomaly_indicators' => $anomalyIndicators,
|
||||
'burst_analysis' => $burstAnalysis,
|
||||
'traffic_analysis' => [
|
||||
'current_rate' => $currentRate,
|
||||
'analysis_window' => $window,
|
||||
'timestamp' => $now,
|
||||
],
|
||||
'confidence' => min(0.95, abs($baselineDeviation) * 0.3 + ($burstAnalysis['burst_intensity'] ?? 0) * 0.7),
|
||||
'client_ip' => $requestContext['client_ip'] ?? null,
|
||||
'request_path' => $requestContext['request_path'] ?? null,
|
||||
'user_agent' => $requestContext['user_agent'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate overall threat score
|
||||
*/
|
||||
private function calculateThreatScore(array $burstAnalysis, float $baselineDeviation, array $requestContext): float
|
||||
{
|
||||
$score = 0.0;
|
||||
|
||||
// Burst pattern scoring
|
||||
if ($burstAnalysis['burst_detected'] ?? false) {
|
||||
$score += ($burstAnalysis['burst_intensity'] ?? 0) * 40;
|
||||
}
|
||||
|
||||
// Baseline deviation scoring
|
||||
if (abs($baselineDeviation) >= 3.0) {
|
||||
$score += min(40, abs($baselineDeviation) * 10);
|
||||
}
|
||||
|
||||
// Suspicious user agent patterns
|
||||
$userAgent = strtolower($requestContext['user_agent'] ?? '');
|
||||
$suspiciousPatterns = ['bot', 'crawler', 'scanner', 'sqlmap', 'nikto', 'nmap'];
|
||||
foreach ($suspiciousPatterns as $pattern) {
|
||||
if (str_contains($userAgent, $pattern)) {
|
||||
$score += 20;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return min(100.0, $score);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identify potential attack patterns
|
||||
*/
|
||||
private function identifyAttackPatterns(array $requestContext, array $burstAnalysis): array
|
||||
{
|
||||
$patterns = [];
|
||||
|
||||
if ($burstAnalysis['burst_detected'] ?? false) {
|
||||
$patterns[] = 'volumetric_attack';
|
||||
}
|
||||
|
||||
if (($burstAnalysis['request_count'] ?? 0) > 100) {
|
||||
$patterns[] = 'flood_attack';
|
||||
}
|
||||
|
||||
$userAgent = strtolower($requestContext['user_agent'] ?? '');
|
||||
if (str_contains($userAgent, 'sqlmap') || str_contains($userAgent, 'scanner')) {
|
||||
$patterns[] = 'automated_attack';
|
||||
}
|
||||
|
||||
return $patterns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identify anomaly indicators
|
||||
*/
|
||||
private function identifyAnomalyIndicators(array $burstAnalysis, float $baselineDeviation): array
|
||||
{
|
||||
$indicators = [];
|
||||
|
||||
if (abs($baselineDeviation) >= 3.0) {
|
||||
$indicators[] = 'traffic_anomaly';
|
||||
}
|
||||
|
||||
if ($burstAnalysis['burst_detected'] ?? false) {
|
||||
$indicators[] = 'burst_pattern';
|
||||
}
|
||||
|
||||
if (($burstAnalysis['min_interval'] ?? 10) < 0.5) {
|
||||
$indicators[] = 'suspicious_timing';
|
||||
}
|
||||
|
||||
return $indicators;
|
||||
}
|
||||
}
|
||||
|
||||
36
src/Framework/RateLimit/RateLimiterInitializer.php
Normal file
36
src/Framework/RateLimit/RateLimiterInitializer.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\RateLimit;
|
||||
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\DI\Container;
|
||||
use App\Framework\DI\Initializer;
|
||||
use App\Framework\RateLimit\Storage\CacheStorage;
|
||||
use App\Framework\SlidingWindow\SlidingWindowFactory;
|
||||
|
||||
final readonly class RateLimiterInitializer
|
||||
{
|
||||
#[Initializer]
|
||||
public function initialize(Container $container): void
|
||||
{
|
||||
// Register SlidingWindow-based rate limiter
|
||||
$container->bind(SlidingWindowRateLimiter::class, function (Container $container) {
|
||||
$cache = $container->get(Cache::class);
|
||||
$windowFactory = new SlidingWindowFactory($cache);
|
||||
|
||||
return new SlidingWindowRateLimiter($windowFactory);
|
||||
});
|
||||
|
||||
// Register legacy rate limiter for backwards compatibility
|
||||
$container->bind(RateLimiter::class, function (Container $container) {
|
||||
$cache = $container->get(Cache::class);
|
||||
|
||||
return new RateLimiter(new CacheStorage($cache));
|
||||
});
|
||||
|
||||
// Alias for default rate limiter (use SlidingWindow version)
|
||||
$container->bind('rate_limiter', fn (Container $container) => $container->get(SlidingWindowRateLimiter::class));
|
||||
}
|
||||
}
|
||||
314
src/Framework/RateLimit/SlidingWindowRateLimiter.php
Normal file
314
src/Framework/RateLimit/SlidingWindowRateLimiter.php
Normal file
@@ -0,0 +1,314 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\RateLimit;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\Core\ValueObjects\Timestamp;
|
||||
use App\Framework\RateLimit\TimeProvider\SystemTimeProvider;
|
||||
use App\Framework\RateLimit\TimeProvider\TimeProviderInterface;
|
||||
use App\Framework\SlidingWindow\Aggregator\RateLimitResult as SlidingWindowRateLimitResult;
|
||||
use App\Framework\SlidingWindow\SlidingWindow;
|
||||
use App\Framework\SlidingWindow\SlidingWindowFactory;
|
||||
|
||||
/**
|
||||
* Modern RateLimiter using SlidingWindow module with advanced analytics
|
||||
*/
|
||||
final readonly class SlidingWindowRateLimiter
|
||||
{
|
||||
private SlidingWindowFactory $windowFactory;
|
||||
|
||||
public function __construct(
|
||||
SlidingWindowFactory $windowFactory,
|
||||
private TimeProviderInterface $timeProvider = new SystemTimeProvider(),
|
||||
private bool $persistent = true
|
||||
) {
|
||||
$this->windowFactory = $windowFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check rate limit with basic counting
|
||||
*/
|
||||
public function checkLimit(string $key, int $limit, int $windowSeconds): RateLimitResult
|
||||
{
|
||||
$window = $this->windowFactory->createCountingWindow(
|
||||
identifier: "rate_limit:{$key}",
|
||||
windowSize: Duration::fromSeconds($windowSeconds),
|
||||
persistent: $this->persistent
|
||||
);
|
||||
|
||||
$now = Timestamp::fromFloat($this->timeProvider->getCurrentTime());
|
||||
|
||||
// Record this request
|
||||
$window->record(1, $now);
|
||||
|
||||
// Get current count
|
||||
$stats = $window->getStats($now);
|
||||
$currentCount = $stats->totalCount;
|
||||
|
||||
if ($currentCount > $limit) {
|
||||
// Calculate retry after based on oldest request in window
|
||||
$values = $window->getValues($now);
|
||||
if (! empty($values)) {
|
||||
$retryAfter = $windowSeconds;
|
||||
} else {
|
||||
$retryAfter = 1;
|
||||
}
|
||||
|
||||
return RateLimitResult::exceeded($limit, $currentCount, $retryAfter);
|
||||
}
|
||||
|
||||
return RateLimitResult::allowed($limit, $currentCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check rate limit with advanced threat analysis
|
||||
*/
|
||||
public function checkLimitWithAnalysis(
|
||||
string $key,
|
||||
int $limit,
|
||||
int $windowSeconds,
|
||||
array $requestContext = []
|
||||
): RateLimitResult {
|
||||
$window = $this->windowFactory->createRateLimitWindow(
|
||||
identifier: "rate_limit_advanced:{$key}",
|
||||
windowSize: Duration::fromSeconds($windowSeconds),
|
||||
persistent: $this->persistent
|
||||
);
|
||||
|
||||
$now = Timestamp::fromFloat($this->timeProvider->getCurrentTime());
|
||||
|
||||
// Record this request
|
||||
$window->record(1, $now);
|
||||
|
||||
// Get advanced analytics
|
||||
$stats = $window->getStats($now);
|
||||
$aggregatedData = $stats->aggregatedData;
|
||||
|
||||
$currentCount = $stats->totalCount;
|
||||
$isAllowed = $currentCount <= $limit;
|
||||
|
||||
if ($aggregatedData['rate_limit'] instanceof SlidingWindowRateLimitResult) {
|
||||
$rateLimitResult = $aggregatedData['rate_limit'];
|
||||
|
||||
// Enhanced analysis with context
|
||||
$analysisData = $this->enhanceAnalysisWithContext(
|
||||
$rateLimitResult->threatAnalysis,
|
||||
$requestContext
|
||||
);
|
||||
|
||||
$retryAfter = $isAllowed ? 0 : $this->calculateRetryAfter($windowSeconds, $rateLimitResult);
|
||||
|
||||
return RateLimitResult::withThreatAnalysis(
|
||||
$isAllowed,
|
||||
$limit,
|
||||
$currentCount,
|
||||
array_merge($analysisData, [
|
||||
'burst_analysis' => $rateLimitResult->getBurstAnalysis(),
|
||||
'sliding_window_stats' => [
|
||||
'window_start' => $stats->windowStart->toFloat(),
|
||||
'window_end' => $stats->windowEnd->toFloat(),
|
||||
'window_size_seconds' => $stats->windowSize->toSeconds(),
|
||||
],
|
||||
]),
|
||||
$retryAfter
|
||||
);
|
||||
}
|
||||
|
||||
$retryAfter = $isAllowed ? 0 : $windowSeconds;
|
||||
|
||||
return RateLimitResult::withThreatAnalysis($isAllowed, $limit, $currentCount, [], $retryAfter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Token bucket implementation using SlidingWindow for tracking
|
||||
*/
|
||||
public function checkTokenBucket(string $key, int $capacity, int $refillRate, int $tokens = 1): RateLimitResult
|
||||
{
|
||||
// For token bucket, we'll use a different approach with the existing TokenBucket class
|
||||
// but track requests using SlidingWindow for analytics
|
||||
$window = $this->windowFactory->createRateLimitWindow(
|
||||
identifier: "token_bucket_tracking:{$key}",
|
||||
windowSize: Duration::fromMinutes(5), // Track for analytics
|
||||
persistent: $this->persistent
|
||||
);
|
||||
|
||||
$now = Timestamp::fromFloat($this->timeProvider->getCurrentTime());
|
||||
|
||||
// Record token consumption request
|
||||
$window->record($tokens, $now);
|
||||
|
||||
// Use traditional token bucket logic but enhance with sliding window analytics
|
||||
$bucket = $this->getOrCreateTokenBucket($key, $capacity, $now->toFloat());
|
||||
|
||||
// Refill tokens
|
||||
$timePassed = $now->toFloat() - $bucket->lastRefill;
|
||||
$tokensToAdd = min($capacity - $bucket->tokens, intval($timePassed * $refillRate));
|
||||
|
||||
$bucket = $bucket->refill($tokensToAdd, intval($now->toFloat()));
|
||||
|
||||
if ($bucket->canConsume($tokens)) {
|
||||
$bucket = $bucket->consume($tokens);
|
||||
$this->saveTokenBucket($key, $bucket);
|
||||
|
||||
return RateLimitResult::allowed($capacity, $bucket->tokens);
|
||||
}
|
||||
|
||||
$this->saveTokenBucket($key, $bucket);
|
||||
$retryAfter = intval(($tokens - $bucket->tokens) / $refillRate);
|
||||
|
||||
return RateLimitResult::exceeded($capacity, $bucket->tokens, $retryAfter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset rate limiting for a key
|
||||
*/
|
||||
public function reset(string $key): void
|
||||
{
|
||||
// Reset both types of windows
|
||||
$countingWindow = $this->windowFactory->createCountingWindow(
|
||||
identifier: "rate_limit:{$key}",
|
||||
windowSize: Duration::fromSeconds(60), // Doesn't matter for reset
|
||||
persistent: $this->persistent
|
||||
);
|
||||
|
||||
$advancedWindow = $this->windowFactory->createRateLimitWindow(
|
||||
identifier: "rate_limit_advanced:{$key}",
|
||||
windowSize: Duration::fromSeconds(60), // Doesn't matter for reset
|
||||
persistent: $this->persistent
|
||||
);
|
||||
|
||||
$countingWindow->clear();
|
||||
$advancedWindow->clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get usage statistics for a key
|
||||
*/
|
||||
public function getUsage(string $key, int $windowSeconds): int
|
||||
{
|
||||
$window = $this->windowFactory->createCountingWindow(
|
||||
identifier: "rate_limit:{$key}",
|
||||
windowSize: Duration::fromSeconds($windowSeconds),
|
||||
persistent: $this->persistent
|
||||
);
|
||||
|
||||
$now = Timestamp::fromFloat($this->timeProvider->getCurrentTime());
|
||||
$stats = $window->getStats($now);
|
||||
|
||||
return $stats->totalCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get advanced analytics for a key
|
||||
*/
|
||||
public function getAdvancedAnalytics(string $key, int $windowSeconds): array
|
||||
{
|
||||
$window = $this->windowFactory->createRateLimitWindow(
|
||||
identifier: "rate_limit_advanced:{$key}",
|
||||
windowSize: Duration::fromSeconds($windowSeconds),
|
||||
persistent: $this->persistent
|
||||
);
|
||||
|
||||
$now = Timestamp::fromFloat($this->timeProvider->getCurrentTime());
|
||||
$stats = $window->getStats($now);
|
||||
|
||||
if (isset($stats->aggregatedData['rate_limit']) &&
|
||||
$stats->aggregatedData['rate_limit'] instanceof SlidingWindowRateLimitResult) {
|
||||
return $stats->aggregatedData['rate_limit']->toArray();
|
||||
}
|
||||
|
||||
return ['count' => $stats->totalCount, 'analytics' => []];
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhance analysis with request context
|
||||
*/
|
||||
private function enhanceAnalysisWithContext(array $threatAnalysis, array $requestContext): array
|
||||
{
|
||||
$enhanced = $threatAnalysis;
|
||||
|
||||
// Add request context analysis
|
||||
if (! empty($requestContext)) {
|
||||
$enhanced['request_context'] = [
|
||||
'client_ip' => $requestContext['client_ip'] ?? null,
|
||||
'user_agent' => $requestContext['user_agent'] ?? null,
|
||||
'request_path' => $requestContext['request_path'] ?? null,
|
||||
];
|
||||
|
||||
// Analyze user agent for suspicious patterns
|
||||
if (isset($requestContext['user_agent'])) {
|
||||
$enhanced['user_agent_analysis'] = $this->analyzeUserAgent($requestContext['user_agent']);
|
||||
}
|
||||
}
|
||||
|
||||
return $enhanced;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze user agent for suspicious patterns
|
||||
*/
|
||||
private function analyzeUserAgent(string $userAgent): array
|
||||
{
|
||||
$suspicious = false;
|
||||
$patterns = [];
|
||||
|
||||
$suspiciousPatterns = [
|
||||
'bot' => '/bot|crawler|spider|scraper/i',
|
||||
'scanner' => '/scan|nikto|nmap|sqlmap|dirb/i',
|
||||
'automation' => '/curl|wget|python|java|go-http/i',
|
||||
];
|
||||
|
||||
foreach ($suspiciousPatterns as $type => $pattern) {
|
||||
if (preg_match($pattern, $userAgent)) {
|
||||
$suspicious = true;
|
||||
$patterns[] = $type;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'suspicious' => $suspicious,
|
||||
'patterns' => $patterns,
|
||||
'user_agent' => $userAgent,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate retry after based on sliding window data
|
||||
*/
|
||||
private function calculateRetryAfter(int $windowSeconds, SlidingWindowRateLimitResult $result): int
|
||||
{
|
||||
if (empty($result->timestamps)) {
|
||||
return $windowSeconds;
|
||||
}
|
||||
|
||||
// Find oldest timestamp and calculate when it will expire
|
||||
$timestamps = $result->getTimestampsAsFloats();
|
||||
$oldestTimestamp = min($timestamps);
|
||||
$now = $this->timeProvider->getCurrentTime();
|
||||
|
||||
$retryAfter = intval($oldestTimestamp + $windowSeconds - $now);
|
||||
|
||||
return max(1, $retryAfter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create token bucket (simplified for demo - in real implementation use storage)
|
||||
*/
|
||||
private function getOrCreateTokenBucket(string $key, int $capacity, float $now): TokenBucket
|
||||
{
|
||||
// This would typically use the storage interface
|
||||
// For now, create a new bucket (not persistent)
|
||||
return new TokenBucket($capacity, $capacity, intval($now));
|
||||
}
|
||||
|
||||
/**
|
||||
* Save token bucket (simplified for demo - in real implementation use storage)
|
||||
*/
|
||||
private function saveTokenBucket(string $key, TokenBucket $bucket): void
|
||||
{
|
||||
// This would typically use the storage interface
|
||||
// For now, this is a no-op (not persistent)
|
||||
}
|
||||
}
|
||||
222
src/Framework/RateLimit/SlidingWindowTokenBucket.php
Normal file
222
src/Framework/RateLimit/SlidingWindowTokenBucket.php
Normal file
@@ -0,0 +1,222 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\RateLimit;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\Core\ValueObjects\Timestamp;
|
||||
use App\Framework\RateLimit\TimeProvider\SystemTimeProvider;
|
||||
use App\Framework\RateLimit\TimeProvider\TimeProviderInterface;
|
||||
use App\Framework\SlidingWindow\SlidingWindow;
|
||||
use App\Framework\SlidingWindow\SlidingWindowFactory;
|
||||
|
||||
/**
|
||||
* Token bucket implementation using SlidingWindow for analytics and tracking
|
||||
*/
|
||||
final readonly class SlidingWindowTokenBucket
|
||||
{
|
||||
private SlidingWindow $tokenWindow;
|
||||
|
||||
private SlidingWindow $analyticsWindow;
|
||||
|
||||
public function __construct(
|
||||
private string $identifier,
|
||||
private int $capacity,
|
||||
private int $refillRate,
|
||||
SlidingWindowFactory $windowFactory,
|
||||
private TimeProviderInterface $timeProvider = new SystemTimeProvider(),
|
||||
private Duration $refillInterval = Duration::SECOND,
|
||||
private bool $persistent = true
|
||||
) {
|
||||
// Window for tracking token consumption (short window for actual limiting)
|
||||
$this->tokenWindow = $windowFactory->createNumericWindow(
|
||||
identifier: "token_bucket:{$this->identifier}",
|
||||
windowSize: Duration::fromMinutes(1), // Short window for token tracking
|
||||
persistent: $this->persistent
|
||||
);
|
||||
|
||||
// Window for analytics and threat detection (longer window)
|
||||
$this->analyticsWindow = $windowFactory->createRateLimitWindow(
|
||||
identifier: "token_analytics:{$this->identifier}",
|
||||
windowSize: Duration::fromMinutes(5), // Longer window for pattern analysis
|
||||
persistent: $this->persistent
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to consume tokens
|
||||
*/
|
||||
public function consume(int $tokens = 1): RateLimitResult
|
||||
{
|
||||
$now = Timestamp::fromFloat($this->timeProvider->getCurrentTime());
|
||||
|
||||
// Record consumption attempt for analytics
|
||||
$this->analyticsWindow->record($tokens, $now);
|
||||
|
||||
// Get current bucket state
|
||||
$currentTokens = $this->getCurrentTokens($now);
|
||||
|
||||
if ($currentTokens >= $tokens) {
|
||||
// Consume tokens by recording the consumption
|
||||
$this->tokenWindow->record($tokens, $now);
|
||||
|
||||
$remainingTokens = $currentTokens - $tokens;
|
||||
|
||||
return RateLimitResult::allowed($this->capacity, $remainingTokens);
|
||||
}
|
||||
|
||||
// Calculate retry after based on refill rate
|
||||
$tokensNeeded = $tokens - $currentTokens;
|
||||
$retryAfter = intval(ceil($tokensNeeded / $this->refillRate) * $this->refillInterval->toSeconds());
|
||||
|
||||
return RateLimitResult::exceeded($this->capacity, $currentTokens, $retryAfter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume tokens with advanced analytics
|
||||
*/
|
||||
public function consumeWithAnalysis(int $tokens = 1, array $requestContext = []): RateLimitResult
|
||||
{
|
||||
$basicResult = $this->consume($tokens);
|
||||
|
||||
// Get analytics from the analytics window
|
||||
$now = Timestamp::fromFloat($this->timeProvider->getCurrentTime());
|
||||
$stats = $this->analyticsWindow->getStats($now);
|
||||
|
||||
$analysisData = [
|
||||
'token_consumption' => [
|
||||
'requested' => $tokens,
|
||||
'available' => $basicResult->getCurrent(),
|
||||
'capacity' => $this->capacity,
|
||||
'refill_rate' => $this->refillRate,
|
||||
],
|
||||
'sliding_window_stats' => [
|
||||
'window_start' => $stats->windowStart->toFloat(),
|
||||
'window_end' => $stats->windowEnd->toFloat(),
|
||||
'total_requests' => $stats->totalCount,
|
||||
],
|
||||
];
|
||||
|
||||
// Add advanced analytics if available
|
||||
if (isset($stats->aggregatedData['rate_limit'])) {
|
||||
$rateLimitResult = $stats->aggregatedData['rate_limit'];
|
||||
if (method_exists($rateLimitResult, 'getBurstAnalysis')) {
|
||||
$analysisData['burst_analysis'] = $rateLimitResult->getBurstAnalysis();
|
||||
$analysisData['threat_analysis'] = $rateLimitResult->threatAnalysis;
|
||||
}
|
||||
}
|
||||
|
||||
// Enhance with request context
|
||||
if (! empty($requestContext)) {
|
||||
$analysisData = array_merge($analysisData, [
|
||||
'client_ip' => $requestContext['client_ip'] ?? null,
|
||||
'user_agent' => $requestContext['user_agent'] ?? null,
|
||||
'request_path' => $requestContext['request_path'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
return RateLimitResult::withThreatAnalysis(
|
||||
$basicResult->isAllowed(),
|
||||
$basicResult->getLimit(),
|
||||
$basicResult->getCurrent(),
|
||||
$analysisData,
|
||||
$basicResult->getRetryAfter()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check current token availability without consuming
|
||||
*/
|
||||
public function peek(): int
|
||||
{
|
||||
$now = Timestamp::fromFloat($this->timeProvider->getCurrentTime());
|
||||
|
||||
return $this->getCurrentTokens($now);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the token bucket
|
||||
*/
|
||||
public function reset(): void
|
||||
{
|
||||
$this->tokenWindow->clear();
|
||||
$this->analyticsWindow->clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bucket capacity
|
||||
*/
|
||||
public function getCapacity(): int
|
||||
{
|
||||
return $this->capacity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get refill rate (tokens per second)
|
||||
*/
|
||||
public function getRefillRate(): int
|
||||
{
|
||||
return $this->refillRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get analytics data
|
||||
*/
|
||||
public function getAnalytics(): array
|
||||
{
|
||||
$now = Timestamp::fromFloat($this->timeProvider->getCurrentTime());
|
||||
$stats = $this->analyticsWindow->getStats($now);
|
||||
|
||||
return [
|
||||
'identifier' => $this->identifier,
|
||||
'capacity' => $this->capacity,
|
||||
'refill_rate' => $this->refillRate,
|
||||
'current_tokens' => $this->getCurrentTokens($now),
|
||||
'analytics_window' => [
|
||||
'total_requests' => $stats->totalCount,
|
||||
'window_start' => $stats->windowStart->toFloat(),
|
||||
'window_end' => $stats->windowEnd->toFloat(),
|
||||
'aggregated_data' => $stats->aggregatedData,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate current available tokens based on refill rate and consumption
|
||||
*/
|
||||
private function getCurrentTokens(Timestamp $now): int
|
||||
{
|
||||
// Get consumption in the last refill period
|
||||
$refillWindowSize = Duration::fromSeconds(
|
||||
intval($this->capacity / $this->refillRate) + 60 // Buffer for precision
|
||||
);
|
||||
|
||||
$tokenStats = $this->tokenWindow->getStats($now);
|
||||
|
||||
// Calculate tokens consumed in the refill window
|
||||
$tokensConsumed = 0;
|
||||
if (isset($tokenStats->aggregatedData['numeric'])) {
|
||||
$numericResult = $tokenStats->aggregatedData['numeric'];
|
||||
if (method_exists($numericResult, 'getSum')) {
|
||||
$tokensConsumed = intval($numericResult->getSum());
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate tokens refilled since the oldest consumption
|
||||
$windowStart = $tokenStats->windowStart->toFloat();
|
||||
$timeSinceWindowStart = $now->toFloat() - $windowStart;
|
||||
$tokensRefilled = min(
|
||||
$this->capacity,
|
||||
intval($timeSinceWindowStart * $this->refillRate)
|
||||
);
|
||||
|
||||
// Available tokens = capacity - consumed + refilled, capped at capacity
|
||||
$availableTokens = min(
|
||||
$this->capacity,
|
||||
max(0, $this->capacity - $tokensConsumed + $tokensRefilled)
|
||||
);
|
||||
|
||||
return $availableTokens;
|
||||
}
|
||||
}
|
||||
@@ -5,38 +5,49 @@ declare(strict_types=1);
|
||||
namespace App\Framework\RateLimit\Storage;
|
||||
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\Cache\CacheKey;
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\RateLimit\TokenBucket;
|
||||
|
||||
final readonly class CacheStorage implements StorageInterface
|
||||
{
|
||||
public function __construct(
|
||||
private Cache $cache
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
public function getRequestsInWindow(string $key, int $windowStart, int $windowEnd): array
|
||||
{
|
||||
$requests = $this->cache->get("rate_limit:$key:requests") ?? [];
|
||||
$cacheKey = CacheKey::fromString("rate_limit:$key:requests");
|
||||
$result = $this->cache->get($cacheKey);
|
||||
$item = $result->getItem($cacheKey);
|
||||
|
||||
if (! $item->isHit || $item->value === false) {
|
||||
$requests = [];
|
||||
} else {
|
||||
$requests = $item->value ?? [];
|
||||
}
|
||||
|
||||
// Alte Requests entfernen
|
||||
$requests = array_filter($requests, fn($timestamp) => $timestamp >= $windowStart);
|
||||
$requests = array_filter($requests, fn ($timestamp) => $timestamp >= $windowStart);
|
||||
|
||||
// Cache aktualisieren
|
||||
$this->cache->set("rate_limit:$key:requests", $requests, 3600);
|
||||
$this->cache->set(\App\Framework\Cache\CacheItem::forSet($cacheKey, $requests, Duration::fromSeconds(3600)));
|
||||
|
||||
return $requests;
|
||||
}
|
||||
|
||||
public function addRequest(string $key, int $timestamp, int $ttl): void
|
||||
{
|
||||
$requests = $this->cache->get("rate_limit:$key:requests") ?? [];
|
||||
$requests = $this->cache->get(CacheKey::fromString("rate_limit:$key:requests"))?->value ?? [];
|
||||
$requests[] = $timestamp;
|
||||
|
||||
$this->cache->set("rate_limit:$key:requests", $requests, $ttl);
|
||||
$this->cache->set(\App\Framework\Cache\CacheItem::forSet(CacheKey::fromString("rate_limit:$key:requests"), $requests, Duration::fromSeconds($ttl)));
|
||||
}
|
||||
|
||||
public function getTokenBucket(string $key): ?TokenBucket
|
||||
{
|
||||
$data = $this->cache->get("rate_limit:$key:bucket");
|
||||
$data = $this->cache->get(CacheKey::fromString("rate_limit:$key:bucket"))->value;
|
||||
|
||||
if ($data === null) {
|
||||
return null;
|
||||
@@ -54,15 +65,42 @@ final readonly class CacheStorage implements StorageInterface
|
||||
$data = [
|
||||
'capacity' => $bucket->capacity,
|
||||
'tokens' => $bucket->tokens,
|
||||
'last_refill' => $bucket->lastRefill
|
||||
'last_refill' => $bucket->lastRefill,
|
||||
];
|
||||
|
||||
$this->cache->set("rate_limit:$key:bucket", $data, 3600);
|
||||
$this->cache->set(\App\Framework\Cache\CacheItem::forSet(CacheKey::fromString("rate_limit:$key:bucket"), $data, Duration::fromSeconds(3600)));
|
||||
}
|
||||
|
||||
public function clear(string $key): void
|
||||
{
|
||||
$this->cache->forget("rate_limit:$key:requests");
|
||||
$this->cache->forget("rate_limit:$key:bucket");
|
||||
$this->cache->forget(CacheKey::fromString("rate_limit:$key:requests"));
|
||||
$this->cache->forget(CacheKey::fromString("rate_limit:$key:bucket"));
|
||||
$this->cache->forget(CacheKey::fromString("rate_limit:$key:baseline"));
|
||||
}
|
||||
|
||||
// ===== WAF Intelligence Extensions =====
|
||||
|
||||
public function getBaseline(string $key): ?array
|
||||
{
|
||||
$baseline = $this->cache->get(CacheKey::fromString("rate_limit:$key:baseline"));
|
||||
|
||||
if ($baseline === null || $baseline->value === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $baseline->value;
|
||||
}
|
||||
|
||||
public function updateBaseline(string $key, int $rate): void
|
||||
{
|
||||
$baseline = $this->getBaseline($key) ?? [];
|
||||
|
||||
// Keep only last 100 measurements for rolling baseline
|
||||
$baseline[] = $rate;
|
||||
if (count($baseline) > 100) {
|
||||
$baseline = array_slice($baseline, -100);
|
||||
}
|
||||
|
||||
$this->cache->set(\App\Framework\Cache\CacheItem::forSet(CacheKey::fromString("rate_limit:$key:baseline"), $baseline, Duration::fromSeconds(7200))); // 2 hours TTL
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,24 @@ use App\Framework\RateLimit\TokenBucket;
|
||||
interface StorageInterface
|
||||
{
|
||||
public function getRequestsInWindow(string $key, int $windowStart, int $windowEnd): array;
|
||||
|
||||
public function addRequest(string $key, int $timestamp, int $ttl): void;
|
||||
|
||||
public function getTokenBucket(string $key): ?TokenBucket;
|
||||
|
||||
public function saveTokenBucket(string $key, TokenBucket $bucket): void;
|
||||
|
||||
public function clear(string $key): void;
|
||||
|
||||
// ===== WAF Intelligence Extensions =====
|
||||
|
||||
/**
|
||||
* Get baseline traffic data for anomaly detection
|
||||
*/
|
||||
public function getBaseline(string $key): ?array;
|
||||
|
||||
/**
|
||||
* Update baseline with new traffic data
|
||||
*/
|
||||
public function updateBaseline(string $key, int $rate): void;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ final readonly class TokenBucket
|
||||
public int $capacity,
|
||||
public int $tokens,
|
||||
public int $lastRefill
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
public function canConsume(int $tokens): bool
|
||||
{
|
||||
@@ -19,7 +20,7 @@ final readonly class TokenBucket
|
||||
|
||||
public function consume(int $tokens): self
|
||||
{
|
||||
if (!$this->canConsume($tokens)) {
|
||||
if (! $this->canConsume($tokens)) {
|
||||
throw new \InvalidArgumentException('Not enough tokens available');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user