""" Application configuration with security improvements and validation """ import os import secrets from datetime import datetime from typing import List, Optional, Dict, Any from pathlib import Path from pydantic import validator, Field from pydantic_settings import BaseSettings from pydantic.networks import AnyHttpUrl, PostgresDsn, RedisDsn import structlog logger = structlog.get_logger(__name__) class Settings(BaseSettings): """Application settings with validation""" # Application PROJECT_NAME: str = "My Uploader Bot" PROJECT_VERSION: str = "2.0.0" PROJECT_HOST: AnyHttpUrl = Field(default="http://127.0.0.1:15100") SANIC_PORT: int = Field(default=15100, ge=1000, le=65535) DEBUG: bool = Field(default=False) # Security SECRET_KEY: str = Field(default_factory=lambda: secrets.token_urlsafe(32)) JWT_SECRET_KEY: str = Field(default_factory=lambda: secrets.token_urlsafe(32)) JWT_EXPIRE_MINUTES: int = Field(default=60 * 24 * 7) # 7 days ENCRYPTION_KEY: Optional[str] = None # Rate Limiting RATE_LIMIT_REQUESTS: int = Field(default=100) RATE_LIMIT_WINDOW: int = Field(default=60) # seconds RATE_LIMIT_ENABLED: bool = Field(default=True) # Database DATABASE_URL: str = Field( default="postgresql+asyncpg://user:password@localhost:5432/uploader_bot" ) DATABASE_POOL_SIZE: int = Field(default=10, ge=1, le=100) DATABASE_MAX_OVERFLOW: int = Field(default=20, ge=0, le=100) DATABASE_ECHO: bool = Field(default=False) # Redis REDIS_URL: RedisDsn = Field(default="redis://localhost:6379/0") REDIS_POOL_SIZE: int = Field(default=10, ge=1, le=100) REDIS_TTL_DEFAULT: int = Field(default=3600) # 1 hour REDIS_TTL_SHORT: int = Field(default=300) # 5 minutes REDIS_TTL_LONG: int = Field(default=86400) # 24 hours # File Storage UPLOADS_DIR: Path = Field(default=Path("/app/data")) MAX_FILE_SIZE: int = Field(default=100 * 1024 * 1024) # 100MB ALLOWED_CONTENT_TYPES: List[str] = Field(default=[ 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'video/mp4', 'video/webm', 'video/ogg', 'video/quicktime', 'audio/mpeg', 'audio/ogg', 'audio/wav', 'audio/mp4', 'text/plain', 'application/json' ]) # Telegram TELEGRAM_API_KEY: str = Field(default="1234567890:ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789") CLIENT_TELEGRAM_API_KEY: str = Field(default="1234567890:ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789") TELEGRAM_WEBHOOK_ENABLED: bool = Field(default=False) TELEGRAM_WEBHOOK_URL: Optional[str] = None TELEGRAM_WEBHOOK_SECRET: str = Field(default_factory=lambda: secrets.token_urlsafe(32)) # TON Blockchain TESTNET: bool = Field(default=False) TONCENTER_HOST: AnyHttpUrl = Field(default="https://toncenter.com/api/v2/") TONCENTER_API_KEY: Optional[str] = None TONCENTER_V3_HOST: AnyHttpUrl = Field(default="https://toncenter.com/api/v3/") MY_PLATFORM_CONTRACT: str = Field(default="EQDmWp6hbJlYUrXZKb9N88sOrTit630ZuRijfYdXEHLtheMY") MY_FUND_ADDRESS: str = Field(default="UQDarChHFMOI2On9IdHJNeEKttqepgo0AY4bG1trw8OAAwMY") # Logging LOG_LEVEL: str = Field(default="INFO", pattern="^(DEBUG|INFO|WARNING|ERROR|CRITICAL)$") LOG_DIR: Path = Field(default=Path("logs")) LOG_FORMAT: str = Field(default="json") LOG_ROTATION: str = Field(default="1 day") LOG_RETENTION: str = Field(default="30 days") # Monitoring METRICS_ENABLED: bool = Field(default=True) METRICS_PORT: int = Field(default=9090, ge=1000, le=65535) HEALTH_CHECK_ENABLED: bool = Field(default=True) # Background Services INDEXER_ENABLED: bool = Field(default=True) INDEXER_INTERVAL: int = Field(default=5, ge=1, le=3600) TON_DAEMON_ENABLED: bool = Field(default=True) TON_DAEMON_INTERVAL: int = Field(default=3, ge=1, le=3600) LICENSE_SERVICE_ENABLED: bool = Field(default=True) LICENSE_SERVICE_INTERVAL: int = Field(default=10, ge=1, le=3600) CONVERT_SERVICE_ENABLED: bool = Field(default=True) CONVERT_SERVICE_INTERVAL: int = Field(default=30, ge=1, le=3600) # Web App URLs WEB_APP_URLS: Dict[str, str] = Field(default={ 'uploadContent': "https://web2-client.vercel.app/uploadContent" }) # Maintenance MAINTENANCE_MODE: bool = Field(default=False) MAINTENANCE_MESSAGE: str = Field(default="System is under maintenance") # Development MOCK_EXTERNAL_SERVICES: bool = Field(default=False) DISABLE_WEBHOOKS: bool = Field(default=False) @validator('UPLOADS_DIR') def create_uploads_dir(cls, v): """Create uploads directory if it doesn't exist and is writable""" try: if not v.exists(): v.mkdir(parents=True, exist_ok=True) except (OSError, PermissionError) as e: # Handle read-only filesystem or permission errors logger.warning(f"Cannot create uploads directory {v}: {e}") # Use current directory as fallback fallback = Path("./data") try: fallback.mkdir(parents=True, exist_ok=True) return fallback except Exception: # Last fallback - current directory return Path(".") return v @validator('LOG_DIR') def create_log_dir(cls, v): """Create log directory if it doesn't exist and is writable""" try: if not v.exists(): v.mkdir(parents=True, exist_ok=True) except (OSError, PermissionError) as e: # Handle read-only filesystem or permission errors logger.warning(f"Cannot create log directory {v}: {e}") # Use current directory as fallback fallback = Path("./logs") try: fallback.mkdir(parents=True, exist_ok=True) return fallback except Exception: # Last fallback - current directory return Path(".") return v @validator('DATABASE_URL') def validate_database_url(cls, v): """Validate database URL format - allow SQLite for testing""" v_str = str(v) if not (v_str.startswith('postgresql+asyncpg://') or v_str.startswith('sqlite+aiosqlite://')): logger.warning(f"Using non-standard database URL: {v_str}") return v @validator('TELEGRAM_API_KEY', 'CLIENT_TELEGRAM_API_KEY') def validate_telegram_keys(cls, v): """Validate Telegram bot tokens format - allow test tokens""" if v.startswith('1234567890:'): # Allow test tokens for development return v parts = v.split(':') if len(parts) != 2 or not parts[0].isdigit() or len(parts[1]) != 35: raise ValueError('Invalid Telegram bot token format') return v @validator('SECRET_KEY', 'JWT_SECRET_KEY') def validate_secret_keys(cls, v): """Validate secret keys length""" if len(v) < 32: raise ValueError('Secret keys must be at least 32 characters long') return v model_config = { "env_file": ".env", "case_sensitive": True, "validate_assignment": True, "extra": "allow" # Allow extra fields from environment } class SecurityConfig: """Security-related configurations""" # CORS settings CORS_ORIGINS = [ "https://web2-client.vercel.app", "https://t.me", "https://web.telegram.org" ] # Content Security Policy CSP_DIRECTIVES = { 'default-src': ["'self'"], 'script-src': ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net"], 'style-src': ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net"], 'img-src': ["'self'", "data:", "https:"], 'connect-src': ["'self'", "https://api.telegram.org"], 'frame-ancestors': ["'none'"], 'form-action': ["'self'"], 'base-uri': ["'self'"] } # Request size limits MAX_REQUEST_SIZE = 100 * 1024 * 1024 # 100MB MAX_JSON_SIZE = 10 * 1024 * 1024 # 10MB # Session settings SESSION_COOKIE_SECURE = True SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_SAMESITE = "Strict" # Rate limiting patterns RATE_LIMIT_PATTERNS = { "auth": {"requests": 5, "window": 300}, # 5 requests per 5 minutes "upload": {"requests": 10, "window": 3600}, # 10 uploads per hour "api": {"requests": 100, "window": 60}, # 100 API calls per minute "heavy": {"requests": 1, "window": 60} # 1 heavy operation per minute } # Create settings instance settings = Settings() # Expose commonly used settings DATABASE_URL = str(settings.DATABASE_URL) REDIS_URL = str(settings.REDIS_URL) DATABASE_POOL_SIZE = settings.DATABASE_POOL_SIZE DATABASE_MAX_OVERFLOW = settings.DATABASE_MAX_OVERFLOW REDIS_POOL_SIZE = settings.REDIS_POOL_SIZE TELEGRAM_API_KEY = settings.TELEGRAM_API_KEY CLIENT_TELEGRAM_API_KEY = settings.CLIENT_TELEGRAM_API_KEY PROJECT_HOST = str(settings.PROJECT_HOST) SANIC_PORT = settings.SANIC_PORT UPLOADS_DIR = settings.UPLOADS_DIR ALLOWED_CONTENT_TYPES = settings.ALLOWED_CONTENT_TYPES TESTNET = settings.TESTNET TONCENTER_HOST = str(settings.TONCENTER_HOST) TONCENTER_API_KEY = settings.TONCENTER_API_KEY TONCENTER_V3_HOST = str(settings.TONCENTER_V3_HOST) MY_PLATFORM_CONTRACT = settings.MY_PLATFORM_CONTRACT MY_FUND_ADDRESS = settings.MY_FUND_ADDRESS LOG_LEVEL = settings.LOG_LEVEL LOG_DIR = settings.LOG_DIR MAINTENANCE_MODE = settings.MAINTENANCE_MODE # Cache keys patterns CACHE_KEYS = { "user_session": "user:session:{user_id}", "user_data": "user:data:{user_id}", "content_metadata": "content:meta:{content_id}", "rate_limit": "rate_limit:{pattern}:{identifier}", "blockchain_task": "blockchain:task:{task_id}", "temp_upload": "upload:temp:{upload_id}", "wallet_connection": "wallet:conn:{wallet_address}", "ton_price": "ton:price:usd", "system_status": "system:status:{service}", } # Log current configuration (without secrets) def log_config(): """Log current configuration without sensitive data""" safe_config = { "project_name": settings.PROJECT_NAME, "project_version": settings.PROJECT_VERSION, "debug": settings.DEBUG, "sanic_port": settings.SANIC_PORT, "testnet": settings.TESTNET, "maintenance_mode": settings.MAINTENANCE_MODE, "metrics_enabled": settings.METRICS_ENABLED, "uploads_dir": str(settings.UPLOADS_DIR), "log_level": settings.LOG_LEVEL, } logger.info("Configuration loaded", **safe_config) # Initialize logging configuration log_config() # Функция для получения настроек (для совместимости с остальным кодом) def get_settings() -> Settings: """ Получить экземпляр настроек приложения. Returns: Settings: Конфигурация приложения """ return settings