Spaces:
Runtime error
Runtime error
| """ | |
| Rate limiting and spam protection middleware | |
| """ | |
| from sqlalchemy.orm import Session | |
| from typing import Optional | |
| from datetime import datetime, timedelta | |
| import hashlib | |
| import json | |
| import logging | |
| from ..models import SubmissionRateLimit, Form | |
| logger = logging.getLogger(__name__) | |
| class SubmissionRateLimiter: | |
| """Rate limiting for form submissions""" | |
| def __init__(self): | |
| self.salt = "autoform_rate_limit_salt" | |
| def check_rate_limit( | |
| self, | |
| db: Session, | |
| form_id: int, | |
| identifier: str, | |
| window_minutes: int = 60, | |
| max_submissions: int = 5 | |
| ) -> bool: | |
| """ | |
| Check if submission rate limit exceeded. | |
| Rules: | |
| - Max 5 submissions per hour per IP for same form (configurable) | |
| - Sliding window implementation | |
| Args: | |
| db: Database session | |
| form_id: ID of the form | |
| identifier: IP hash or device fingerprint | |
| window_minutes: Time window in minutes | |
| max_submissions: Maximum submissions allowed in window | |
| Returns: | |
| True if rate limit is OK (allowed), False if exceeded | |
| """ | |
| try: | |
| # Calculate window start time | |
| window_start = datetime.utcnow() - timedelta(minutes=window_minutes) | |
| # Check if there's an existing rate limit record | |
| rate_limit = db.query(SubmissionRateLimit).filter( | |
| SubmissionRateLimit.identifier == identifier, | |
| SubmissionRateLimit.form_id == form_id, | |
| SubmissionRateLimit.window_start >= window_start | |
| ).first() | |
| if rate_limit: | |
| # Check if blocked | |
| if rate_limit.is_blocked: | |
| logger.warning(f"Rate limit blocked: {identifier} for form {form_id}") | |
| return False | |
| # Check submission count | |
| if rate_limit.submission_count >= max_submissions: | |
| # Block this identifier | |
| rate_limit.is_blocked = True | |
| db.commit() | |
| logger.warning(f"Rate limit exceeded: {identifier} for form {form_id}") | |
| return False | |
| # Increment count | |
| rate_limit.submission_count += 1 | |
| db.commit() | |
| return True | |
| else: | |
| # Create new rate limit record | |
| new_rate_limit = SubmissionRateLimit( | |
| identifier=identifier, | |
| form_id=form_id, | |
| submission_count=1, | |
| window_start=datetime.utcnow(), | |
| is_blocked=False | |
| ) | |
| db.add(new_rate_limit) | |
| db.commit() | |
| return True | |
| except Exception as e: | |
| logger.error(f"Error checking rate limit: {e}") | |
| # On error, allow submission (fail open) | |
| return True | |
| def check_duplicate( | |
| self, | |
| db: Session, | |
| form_id: int, | |
| answers_hash: str, | |
| window_minutes: int = 5 | |
| ) -> bool: | |
| """ | |
| Check for duplicate submissions (same answers within time window). | |
| Args: | |
| db: Database session | |
| form_id: ID of the form | |
| answers_hash: SHA256 hash of sorted answers | |
| window_minutes: Time window to check for duplicates | |
| Returns: | |
| True if duplicate found, False otherwise | |
| """ | |
| # TODO: Implement duplicate detection | |
| # Store hashes in rate_limits table or separate duplicate_submissions table | |
| # For now, return False (no duplicate) | |
| return False | |
| def hash_identifier(self, identifier: str) -> str: | |
| """Hash identifier (IP address) for storage""" | |
| if not identifier: | |
| return "unknown" | |
| salted = f"{identifier}{self.salt}" | |
| return hashlib.sha256(salted.encode()).hexdigest() | |
| def hash_answers(self, answers: list) -> str: | |
| """Hash answers for duplicate detection""" | |
| # Sort and serialize answers for consistent hashing | |
| sorted_answers = sorted( | |
| answers, | |
| key=lambda x: x.get('question_id', 0) | |
| ) | |
| # Create string representation | |
| answer_str = json.dumps(sorted_answers, sort_keys=True) | |
| return hashlib.sha256(answer_str.encode()).hexdigest() | |
| def cleanup_old_records( | |
| self, | |
| db: Session, | |
| days_old: int = 7 | |
| ): | |
| """ | |
| Clean up old rate limit records. | |
| Args: | |
| db: Database session | |
| days_old: Delete records older than this many days | |
| """ | |
| try: | |
| cutoff_date = datetime.utcnow() - timedelta(days=days_old) | |
| deleted_count = db.query(SubmissionRateLimit).filter( | |
| SubmissionRateLimit.window_start < cutoff_date | |
| ).delete() | |
| db.commit() | |
| if deleted_count > 0: | |
| logger.info(f"Cleaned up {deleted_count} old rate limit records") | |
| except Exception as e: | |
| logger.error(f"Error cleaning up rate limit records: {e}") | |
| db.rollback() | |
| # Singleton instance | |
| rate_limiter = SubmissionRateLimiter() | |