""" 🔗 PROXY SERVER 3.5 - ПУБЛИЧНЫЙ ПРОКСИ С ЛИЦЕНЗИОННОЙ СИСТЕМОЙ Получает данные от Safe Backend, кэширует, отправляет в МТ5 с проверкой лицензий """ from fastapi import FastAPI, HTTPException, Request, Query from fastapi.middleware.cors import CORSMiddleware import httpx import os import time from datetime import datetime from typing import Optional, Dict, Any import uvicorn import hashlib # ==================== КОНФИГУРАЦИЯ ==================== class Config: SAFE_BACKEND_URL = os.environ.get("SAFE_BACKEND_URL", "").rstrip("/") SAFE_API_TOKEN = os.environ.get("SAFE_API_TOKEN", "") HF_TOKEN = os.environ.get("HF_TOKEN", "") # Токен для доступа к приватному Space CACHE_TTL = 60 # 1 минута (быстрый кэш для MT5) MAX_REQUESTS_PER_MINUTE = 2000 # Лимит для MT5 советников TRIAL_KEY = "TRIAL_KEY_3DAYS" # Триальный ключ # ==================== ПРОВЕРКА ==================== print("\n" + "="*90) print("🚀 PROXY SERVER 3.5 - ИНИЦИАЛИЗАЦИЯ") print("="*90) if not Config.SAFE_BACKEND_URL: print("❌ SAFE_BACKEND_URL не установлен в Secrets!") print("⚠️ Должен быть вида: https://username-safe-backend.hf.space") exit(1) if not Config.SAFE_API_TOKEN: print("❌ SAFE_API_TOKEN не установлен в Secrets!") exit(1) if not Config.HF_TOKEN: print("⚠️ HF_TOKEN не установлен - не сможем достучаться до приватного Space!") print(f"✅ Safe Backend: {Config.SAFE_BACKEND_URL}") print(f"✅ API Токен: {Config.SAFE_API_TOKEN[:20]}...") print(f"✅ HF Токен: {Config.HF_TOKEN[:20] + '...' if Config.HF_TOKEN else 'не установлен'}") print(f"✅ Триальный ключ: {Config.TRIAL_KEY}") print(f"✅ Кэш TTL: {Config.CACHE_TTL} сек") print(f"✅ Rate Limit: {Config.MAX_REQUESTS_PER_MINUTE}/мин") print("="*90) # ==================== FASTAPI ==================== app = FastAPI( title="Gold Proxy Server 3.5", version="3.5", docs_url="/docs", redoc_url="/redoc" ) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # ==================== ЛОГИРОВАНИЕ ПРОКСИ ==================== class ProxyLogger: def __init__(self): self.requests = [] def log_request(self, license_key: str, dte: int, client_ip: str, status: str, cache_hit: bool, client_id: str = None, plan: str = None): """Логирует запросы""" entry = { "timestamp": datetime.now().isoformat(), "license_key": license_key, "client_id": client_id, "plan": plan, "dte": dte, "client_ip": client_ip, "status": status, "cache_hit": cache_hit } self.requests.append(entry) # Сокращаем для красоты lic_display = license_key[:10] + "..." if len(license_key) > 10 else license_key client_display = client_id[:8] + "..." if client_id and len(client_id) > 8 else client_id or "N/A" print(f"📊 [{datetime.now().strftime('%H:%M:%S')}] {lic_display} | {client_display} | DTE={dte} {status} (cache: {'HIT' if cache_hit else 'MISS'})") def get_stats(self) -> Dict: """Статистика""" if not self.requests: return {"total": 0, "hits": 0, "misses": 0} hits = len([r for r in self.requests if r['cache_hit']]) misses = len([r for r in self.requests if not r['cache_hit']]) # Группировка по планам plans = {} for r in self.requests: plan = r.get('plan', 'unknown') plans[plan] = plans.get(plan, 0) + 1 return { "total_requests": len(self.requests), "cache_hits": hits, "cache_misses": misses, "hit_rate": round(hits / (hits + misses) * 100, 1) if (hits + misses) > 0 else 0, "requests_by_plan": plans, "last_requests": self.requests[-10:] } proxy_logger = ProxyLogger() # ==================== КЭШИРОВАНИЕ ==================== class ProxyCache: def __init__(self): self.cache = {} self.client = None self.stats = { 'backend_calls': 0, 'errors': 0, 'last_success': 0, 'backend_available': False, 'trial_requests': 0, 'paid_requests': 0 } async def initialize(self): """Инициализация""" self.client = httpx.AsyncClient( timeout=30.0, limits=httpx.Limits(max_connections=100, max_keepalive_connections=20), follow_redirects=True ) print("✅ HTTP клиент инициализирован") await self._test_backend() async def _test_backend(self): """Проверка Safe Backend""" print("🔗 Проверка соединения с Safe Backend...") try: headers = { "User-Agent": "Gold-Proxy/3.5", "X-API-Token": Config.SAFE_API_TOKEN, } if Config.HF_TOKEN: headers["Authorization"] = f"Bearer {Config.HF_TOKEN}" print(f" URL: {Config.SAFE_BACKEND_URL}") print(f" Headers: X-API-Token={Config.SAFE_API_TOKEN[:15]}..., Authorization=Bearer {Config.HF_TOKEN[:15] if Config.HF_TOKEN else 'NONE'}...") # Проверяем health endpoint response = await self.client.get( f"{Config.SAFE_BACKEND_URL}/health", headers=headers, timeout=10.0, follow_redirects=True ) if response.status_code == 200: data = response.json() print(f" ✅ Статус: {response.status_code}") print(f" ✅ Ответ: {data.get('status', 'unknown')}") print(f" ✅ Лицензий: {data.get('licenses_count', 0)}") print(f" ✅ Триальных пользователей: {data.get('trial_users_count', 0)}") self.stats['backend_available'] = True print(f"\n ✅✅✅ Safe Backend РАБОТАЕТ! ✅✅✅\n") return True else: print(f" ❌ {response.status_code}: {response.text[:200]}") return False except Exception as e: print(f" ❌ КРИТИЧЕСКАЯ ОШИБКА: {e}") import traceback print(f" Traceback: {traceback.format_exc()}") return False def _make_key(self, license_key: str, dte: int, client_id: str = None) -> str: """Создает ключ кэша""" if client_id: return f"{license_key}_{client_id}_{dte}" return f"{license_key}_{dte}" async def _fetch_from_backend(self, license_key: str, dte: int, client_id: str = None) -> Optional[Dict]: """Запрос к Safe Backend""" if not self.client: return None url = f"{Config.SAFE_BACKEND_URL}/api/mt5/data" headers = { "X-API-Token": Config.SAFE_API_TOKEN, "User-Agent": "Gold-Proxy/3.5", } if Config.HF_TOKEN: headers["Authorization"] = f"Bearer {Config.HF_TOKEN}" params = { "license_key": license_key, "dte": dte } # Добавляем client_id для триального ключа if license_key == Config.TRIAL_KEY and client_id: params["client_id"] = client_id try: print(f" 📡 Запрос к Safe Backend: {url}") print(f" 📋 Параметры: license_key={license_key[:10]}..., dte={dte}, client_id={client_id[:10] + '...' if client_id and len(client_id) > 10 else client_id}") response = await self.client.get( url, params=params, headers=headers, timeout=15.0 ) if response.status_code == 200: data = response.json() self.stats['backend_calls'] += 1 self.stats['last_success'] = time.time() self.stats['backend_available'] = True # Статистика по типу запросов if license_key == Config.TRIAL_KEY: self.stats['trial_requests'] += 1 else: self.stats['paid_requests'] += 1 print(f" ✅ Успешно: {len(data.get('data', {}).get('calls', []))} calls, {len(data.get('data', {}).get('puts', []))} puts") return data elif response.status_code == 401: print(f" 🔐 401: Неверный токен или лицензия!") # Пытаемся получить детали ошибки try: error_data = response.json() print(f" Ошибка: {error_data.get('detail', 'Unknown')[:100]}") except: pass return None elif response.status_code == 403: print(f" 🔒 403: Доступ запрещен!") return None elif response.status_code == 429: print(f" ⚠️ 429: Превышен лимит запросов!") return None else: print(f" ❌ {response.status_code}: {response.text[:100]}") return None except Exception as e: self.stats['errors'] += 1 print(f" ❌ Ошибка запроса: {type(e).__name__}: {str(e)}") return None async def get(self, license_key: str, dte: int, client_id: str = None) -> Optional[Dict]: """Получает данные""" cache_key = self._make_key(license_key, dte, client_id) current_time = time.time() # Проверяем кэш if cache_key in self.cache: entry = self.cache[cache_key] if current_time - entry['timestamp'] < Config.CACHE_TTL: age = current_time - entry['timestamp'] print(f" 💾 Кэш HIT (возраст: {age:.1f}s)") return entry['data'] # Запрашиваем от бэкенда print(f" 📡 Запрос к Safe Backend...") data = await self._fetch_from_backend(license_key, dte, client_id) if data: self.cache[cache_key] = { 'data': data, 'timestamp': current_time, 'source': 'backend', 'license_key': license_key, 'client_id': client_id } return data # Используем старые данные если есть (stale cache) if cache_key in self.cache: entry = self.cache[cache_key] age = current_time - entry['timestamp'] if age < Config.CACHE_TTL * 3: # Используем до 3 минут print(f" ⚠️ Кэш STALE (возраст: {age:.1f}s, лимит: {Config.CACHE_TTL * 3}s)") return entry['data'] else: print(f" ❌ Кэш слишком старый ({age:.1f}s), не используем") return None def get_stats(self) -> Dict: """Статистика""" now = time.time() backend_age = round(now - self.stats['last_success'], 1) if self.stats['last_success'] > 0 else 9999 # Анализ кэша cache_info = {} for key, entry in list(self.cache.items())[:10]: cache_info[key] = { 'age': round(now - entry['timestamp'], 1), 'license': entry['license_key'], 'client_id': entry['client_id'][:8] + '...' if entry['client_id'] and len(entry['client_id']) > 8 else entry['client_id'] } return { 'cache_size': len(self.cache), 'backend_calls': self.stats['backend_calls'], 'trial_requests': self.stats['trial_requests'], 'paid_requests': self.stats['paid_requests'], 'errors': self.stats['errors'], 'backend_available': self.stats['backend_available'], 'backend_age_seconds': backend_age, 'backend_status': 'healthy' if backend_age < 300 else 'unhealthy', 'cached_entries': cache_info } cache = ProxyCache() # ==================== RATE LIMITER ==================== class RateLimiter: def __init__(self): self.requests = {} def check_limit(self, client_ip: str) -> bool: now = time.time() if client_ip not in self.requests: self.requests[client_ip] = [] # Удаляем старые записи (старше 60 секунд) self.requests[client_ip] = [ ts for ts in self.requests[client_ip] if now - ts < 60 ] if len(self.requests[client_ip]) >= Config.MAX_REQUESTS_PER_MINUTE: print(f" ⚠️ Rate limit для IP {client_ip}: {len(self.requests[client_ip])}/{Config.MAX_REQUESTS_PER_MINUTE}") return False self.requests[client_ip].append(now) return True rate_limiter = RateLimiter() # ==================== API ==================== @app.get("/") async def root(): """Главная страница""" cache_stats = cache.get_stats() proxy_stats = proxy_logger.get_stats() return { "name": "Gold Proxy Server 3.5", "version": "3.5", "status": "online", "architecture": "MT5 → Proxy (публичный) → Safe Backend (приватный) → Firebase", "backend_url": Config.SAFE_BACKEND_URL, "trial_key": Config.TRIAL_KEY, "cache_stats": cache_stats, "proxy_stats": proxy_stats, "config": { "cache_ttl": Config.CACHE_TTL, "rate_limit": Config.MAX_REQUESTS_PER_MINUTE, "has_hf_token": bool(Config.HF_TOKEN) }, "contacts": { "email": "omaralinovaskar95@gmail.com", "website": "https://askhat707.github.io/gold-options-site/", "buy_url": "https://askhat707.github.io/gold-options-site/buy" }, "timestamp": datetime.now().isoformat() } @app.get("/health") async def health(): """Проверка здоровья""" cache_stats = cache.get_stats() backend_ok = cache_stats['backend_age_seconds'] < 300 return { "status": "healthy" if backend_ok else "warning", "backend_connection": backend_ok, "backend_age_seconds": cache_stats['backend_age_seconds'], "cache": cache_stats, "proxy": proxy_logger.get_stats(), "timestamp": datetime.now().isoformat() } @app.get("/test") async def test(): """Тестовый эндпоинт""" print("\n🧪 ТЕСТОВЫЙ ЗАПРОС...") test_cases = [ {"license_key": Config.TRIAL_KEY, "client_id": "TEST_CLIENT_001"}, {"license_key": "TEST_KEY_12345", "client_id": None}, {"license_key": "CLIENT_PRO_001", "client_id": None} ] results = {} for i, test_case in enumerate(test_cases): print(f"\n🔍 Тест #{i+1}: {test_case['license_key']}") data = await cache.get(test_case["license_key"], 0, test_case["client_id"]) if data: results[test_case["license_key"]] = { "success": True, "calls": len(data['data']['calls']), "puts": len(data['data']['puts']), "license_plan": data.get('license_info', {}).get('plan', 'unknown'), "warning": data.get('warning', 'none') } else: results[test_case["license_key"]] = {"success": False} return { "status": "ok", "message": "Proxy тестирование", "results": results, "cache_stats": cache.get_stats(), "timestamp": datetime.now().isoformat() } @app.get("/api/mt5/data") async def proxy_mt5_data( request: Request, license_key: str = Query(..., description="Ключ лицензии"), dte: int = Query(0, description="Days to Expiration (0, 1, 2)"), client_id: Optional[str] = Query(None, description="Идентификатор клиента (обязателен для триального ключа)") ): """ Основной API для МТ5 СОВЕТНИКОВ Параметры: - license_key: ключ лицензии (TRIAL_KEY_3DAYS для триала, или купленный ключ) - dte: 0, 1 или 2 - client_id: обязателен для триального ключа """ client_ip = request.client.host if request.client else "unknown" print(f"\n📥 МТ5 ЗАПРОС") print(f" IP: {client_ip}") print(f" Лицензия: {license_key}") print(f" Client ID: {client_id}") print(f" DTE: {dte}") # Проверка DTE if dte not in [0, 1, 2]: proxy_logger.log_request(license_key, dte, client_ip, "ERROR", False, client_id) raise HTTPException(status_code=400, detail="DTE must be 0, 1, or 2") # Проверка client_id для триального ключа if license_key == Config.TRIAL_KEY and not client_id: proxy_logger.log_request(license_key, dte, client_ip, "NO_CLIENT_ID", False, client_id) raise HTTPException( status_code=400, detail="Для триального ключа необходим client_id. Перезагрузите советник." ) # Rate limit if not rate_limiter.check_limit(client_ip): proxy_logger.log_request(license_key, dte, client_ip, "RATE_LIMIT", False, client_id) raise HTTPException( status_code=429, detail=f"Rate limit exceeded ({Config.MAX_REQUESTS_PER_MINUTE}/min)" ) # Получаем данные data = await cache.get(license_key, dte, client_id) if not data: proxy_logger.log_request(license_key, dte, client_ip, "BACKEND_ERROR", False, client_id) # Более информативное сообщение об ошибке error_detail = "Не удалось получить данные от сервера. Попробуйте позже." if license_key == Config.TRIAL_KEY: error_detail += "\n\nВозможно, ваш триальный период истек." error_detail += "\n📧 Контакты: omaralinovaskar95@gmail.com" error_detail += "\n🌐 Сайт: https://askhat707.github.io/gold-options-site/" raise HTTPException( status_code=503, detail=error_detail ) # Логируем успешный запрос cache_key = cache._make_key(license_key, dte, client_id) cache_hit = False if cache_key in cache.cache: entry = cache.cache[cache_key] cache_hit = (time.time() - entry['timestamp'] < Config.CACHE_TTL) # Получаем план для логирования plan = data.get('license_info', {}).get('plan', 'unknown') proxy_logger.log_request(license_key, dte, client_ip, "OK", cache_hit, client_id, plan) print(f"📤 Ответ: {len(data['data']['calls'])} calls, {len(data['data']['puts'])} puts") print(f" План: {plan}, Осталось запросов: {data['license_info'].get('requests_remaining', 'N/A')}") if data.get('warning'): print(f" ⚠️ Предупреждение: {data['warning']}") return data # ==================== ЭНДПОИНТ ДЛЯ ПРОВЕРКИ ЛИЦЕНЗИИ ==================== @app.get("/api/license/check") async def check_license( license_key: str = Query(...), client_id: Optional[str] = Query(None) ): """Проверка лицензии через прокси""" url = f"{Config.SAFE_BACKEND_URL}/api/license/check" params = { "license_key": license_key } if license_key == Config.TRIAL_KEY and client_id: params["client_id"] = client_id headers = { "X-API-Token": Config.SAFE_API_TOKEN, "User-Agent": "Gold-Proxy/3.5", } if Config.HF_TOKEN: headers["Authorization"] = f"Bearer {Config.HF_TOKEN}" try: async with httpx.AsyncClient(timeout=10.0) as client: response = await client.get(url, params=params, headers=headers) if response.status_code == 200: return response.json() else: return { "is_valid": False, "message": f"Ошибка проверки лицензии: {response.status_code}", "license": { "key": license_key, "plan": "unknown", "active": False } } except Exception as e: return { "is_valid": False, "message": f"Ошибка соединения: {str(e)}", "license": { "key": license_key, "plan": "unknown", "active": False } } @app.on_event("startup") async def startup_event(): await cache.initialize() print("\n" + "="*90) print("🚀 PROXY SERVER 3.5 ЗАПУЩЕН") print("="*90) print(f"Время: {datetime.now().isoformat()}") print(f"Safe Backend: {Config.SAFE_BACKEND_URL}") print(f"Триальный ключ: {Config.TRIAL_KEY}") print(f"Кэш TTL: {Config.CACHE_TTL} сек") print(f"Rate Limit: {Config.MAX_REQUESTS_PER_MINUTE}/мин") print(f"Архитектура:") print(" ┌─────────┐ 1000 запросов/мин ┌─────────────┐ 1 запрос/мин ┌──────────────────┐ 1 запрос/3 мин ┌─────────────┐") print(" │ MT5 │─────────────────────────▶│ PROXY │────────────────────▶│ SAFE BACKEND │────────────────────▶│ FIREBASE │") print(" │Советник │◀─────────────────────────│ (публичный)│◀───────────────────│ (приватный) │◀───────────────────│ │") print(" └─────────┘ данные из кэша прокси └─────────────┘ данные из кэша бэкенда└──────────────────┘ реальные данные └─────────────┘") print("="*90) print("📌 ИНФОРМАЦИЯ ДЛЯ ПОЛЬЗОВАТЕЛЕЙ:") print(f" Триальный ключ: {Config.TRIAL_KEY}") print(f" Триальный период: 3 дня") print(f" Проверка лицензии: каждые 15 минут") print(f" Контакты: omaralinovaskar95@gmail.com") print(f" Сайт: https://askhat707.github.io/gold-options-site/") print("="*90) if __name__ == "__main__": port = int(os.environ.get("PORT", 7860)) uvicorn.run(app, host="0.0.0.0", port=port, log_level="info")