Dieser Commit ist enthalten in:
Claude Project Manager
2025-07-05 17:51:16 +02:00
Commit 0d7d888502
1594 geänderte Dateien mit 122839 neuen und 0 gelöschten Zeilen

Datei anzeigen

@ -0,0 +1 @@
# Middleware Module

Datei anzeigen

@ -0,0 +1,158 @@
import time
from functools import wraps
from flask import request, jsonify
import redis
from typing import Optional, Tuple
import logging
logger = logging.getLogger(__name__)
class RateLimiter:
"""Rate limiting middleware using Redis"""
def __init__(self, redis_url: str):
self.redis_client = None
try:
self.redis_client = redis.from_url(redis_url, decode_responses=True)
self.redis_client.ping()
logger.info("Connected to Redis for rate limiting")
except Exception as e:
logger.warning(f"Redis not available for rate limiting: {e}")
def limit(self, requests_per_minute: int = 60, requests_per_hour: int = 1000):
"""Decorator for rate limiting endpoints"""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not self.redis_client:
# Redis not available, skip rate limiting
return f(*args, **kwargs)
# Get client identifier (API key or IP)
client_id = self._get_client_id()
# Check rate limits
is_allowed, retry_after = self._check_rate_limit(
client_id,
requests_per_minute,
requests_per_hour
)
if not is_allowed:
response = jsonify({
"error": "Rate limit exceeded",
"retry_after": retry_after
})
response.status_code = 429
response.headers['Retry-After'] = str(retry_after)
response.headers['X-RateLimit-Limit'] = str(requests_per_minute)
return response
# Add rate limit headers
response = f(*args, **kwargs)
if hasattr(response, 'headers'):
response.headers['X-RateLimit-Limit'] = str(requests_per_minute)
response.headers['X-RateLimit-Remaining'] = str(
self._get_remaining_requests(client_id, requests_per_minute)
)
return response
return decorated_function
return decorator
def _get_client_id(self) -> str:
"""Get client identifier from request"""
# First try API key
api_key = request.headers.get('X-API-Key')
if api_key:
return f"api_key:{api_key}"
# Then try auth token
auth_header = request.headers.get('Authorization')
if auth_header and auth_header.startswith('Bearer '):
return f"token:{auth_header[7:32]}" # Use first 32 chars of token
# Fallback to IP
if request.headers.get('X-Forwarded-For'):
ip = request.headers.get('X-Forwarded-For').split(',')[0]
else:
ip = request.remote_addr
return f"ip:{ip}"
def _check_rate_limit(self, client_id: str,
requests_per_minute: int,
requests_per_hour: int) -> Tuple[bool, Optional[int]]:
"""Check if request is within rate limits"""
now = int(time.time())
# Check minute limit
minute_key = f"rate_limit:minute:{client_id}:{now // 60}"
minute_count = self.redis_client.incr(minute_key)
self.redis_client.expire(minute_key, 60)
if minute_count > requests_per_minute:
retry_after = 60 - (now % 60)
return False, retry_after
# Check hour limit
hour_key = f"rate_limit:hour:{client_id}:{now // 3600}"
hour_count = self.redis_client.incr(hour_key)
self.redis_client.expire(hour_key, 3600)
if hour_count > requests_per_hour:
retry_after = 3600 - (now % 3600)
return False, retry_after
return True, None
def _get_remaining_requests(self, client_id: str, limit: int) -> int:
"""Get remaining requests in current minute"""
now = int(time.time())
minute_key = f"rate_limit:minute:{client_id}:{now // 60}"
try:
current_count = int(self.redis_client.get(minute_key) or 0)
return max(0, limit - current_count)
except:
return limit
class APIKeyRateLimiter(RateLimiter):
"""Extended rate limiter with API key specific limits"""
def __init__(self, redis_url: str, db_repo):
super().__init__(redis_url)
self.db_repo = db_repo
def limit_by_api_key(self):
"""Rate limit based on API key configuration"""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
api_key = request.headers.get('X-API-Key')
if not api_key:
# Use default limits for non-API key requests
return self.limit()(f)(*args, **kwargs)
# Get API key configuration from database
query = """
SELECT rate_limit_per_minute, rate_limit_per_hour
FROM api_clients
WHERE api_key = %s AND is_active = true
"""
client = self.db_repo.execute_one(query, (api_key,))
if not client:
return jsonify({"error": "Invalid API key"}), 401
# Use custom limits or defaults
rpm = client.get('rate_limit_per_minute', 60)
rph = client.get('rate_limit_per_hour', 1000)
return self.limit(rpm, rph)(f)(*args, **kwargs)
return decorated_function
return decorator