294 lines
11 KiB
Python
294 lines
11 KiB
Python
"""
|
||
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 |