Spaces:
Paused
Paused
| """ | |
| 🔗 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 ==================== | |
| 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() | |
| } | |
| 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() | |
| } | |
| 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() | |
| } | |
| 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 | |
| # ==================== ЭНДПОИНТ ДЛЯ ПРОВЕРКИ ЛИЦЕНЗИИ ==================== | |
| 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 | |
| } | |
| } | |
| 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") |